From 2a02077e3797ce12f8edb478d4335ed4b87c9d92 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 09:32:38 -0800 Subject: [PATCH 01/20] Add name of test in all describe blocks in it --- src/testRunner/tsconfig.json | 3 +- .../unittests/commandLineParsing.ts | 4 +-- src/testRunner/unittests/compileOnSave.ts | 4 +-- src/testRunner/unittests/moduleResolution.ts | 14 ++++---- .../{programMissingFiles.ts => programApi.ts} | 28 +++++++++++++++ .../unittests/programNoParseFalsyFileNames.ts | 36 ------------------- src/testRunner/unittests/projectErrors.ts | 2 +- src/testRunner/unittests/projectReferences.ts | 2 +- .../unittests/reuseProgramStructure.ts | 6 ++-- src/testRunner/unittests/session.ts | 10 +++--- src/testRunner/unittests/tsconfigParsing.ts | 2 +- src/testRunner/unittests/typingsInstaller.ts | 18 +++++----- 12 files changed, 60 insertions(+), 69 deletions(-) rename src/testRunner/unittests/{programMissingFiles.ts => programApi.ts} (79%) delete mode 100644 src/testRunner/unittests/programNoParseFalsyFileNames.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 32772ed311b..10b8637aede 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -68,8 +68,7 @@ "unittests/parsePseudoBigInt.ts", "unittests/paths.ts", "unittests/printer.ts", - "unittests/programMissingFiles.ts", - "unittests/programNoParseFalsyFileNames.ts", + "unittests/programApi.ts", "unittests/projectErrors.ts", "unittests/projectReferences.ts", "unittests/publicApi.ts", diff --git a/src/testRunner/unittests/commandLineParsing.ts b/src/testRunner/unittests/commandLineParsing.ts index 7e2090eb3bd..7e8ba8f84bb 100644 --- a/src/testRunner/unittests/commandLineParsing.ts +++ b/src/testRunner/unittests/commandLineParsing.ts @@ -1,5 +1,5 @@ namespace ts { - describe("parseCommandLine", () => { + describe("commandLineParsing:: parseCommandLine", () => { function assertParseResult(commandLine: string[], expectedParsedCommandLine: ParsedCommandLine) { const parsed = parseCommandLine(commandLine); @@ -367,7 +367,7 @@ namespace ts { }); }); - describe("parseBuildOptions", () => { + describe("commandLineParsing:: parseBuildOptions", () => { function assertParseResult(commandLine: string[], expectedParsedBuildCommand: ParsedBuildCommand) { const parsed = parseBuildCommand(commandLine); const parsedBuildOptions = JSON.stringify(parsed.buildOptions); diff --git a/src/testRunner/unittests/compileOnSave.ts b/src/testRunner/unittests/compileOnSave.ts index 4b98d71ef0a..07a0aa5d3ae 100644 --- a/src/testRunner/unittests/compileOnSave.ts +++ b/src/testRunner/unittests/compileOnSave.ts @@ -6,7 +6,7 @@ namespace ts.projectSystem { return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host); } - describe("CompileOnSave affected list", () => { + describe("compileOnSave:: affected list", () => { function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: File[] }[]) { const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[]; const actualResult = response.sort((list1, list2) => compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); @@ -504,7 +504,7 @@ namespace ts.projectSystem { }); }); - describe("EmitFile test", () => { + describe("compileOnSave:: EmitFile test", () => { it("should respect line endings", () => { test("\n"); test("\r\n"); diff --git a/src/testRunner/unittests/moduleResolution.ts b/src/testRunner/unittests/moduleResolution.ts index 05e17384978..28f25437149 100644 --- a/src/testRunner/unittests/moduleResolution.ts +++ b/src/testRunner/unittests/moduleResolution.ts @@ -80,7 +80,7 @@ namespace ts { } } - describe("Node module resolution - relative paths", () => { + describe("moduleResolution:: Node module resolution - relative paths", () => { function testLoadAsFile(containingFileName: string, moduleFileNameNoExt: string, moduleName: string): void { for (const ext of supportedTSExtensions) { @@ -200,7 +200,7 @@ namespace ts { }); }); - describe("Node module resolution - non-relative paths", () => { + describe("moduleResolution:: Node module resolution - non-relative paths", () => { it("computes correct commonPrefix for moduleName cache", () => { const resolutionCache = createModuleResolutionCache("/", (f) => f); let cache = resolutionCache.getOrCreateCacheForModuleName("a"); @@ -457,7 +457,7 @@ namespace ts { }); }); - describe("Module resolution - relative imports", () => { + describe("moduleResolution:: Relative imports", () => { function test(files: Map, currentDirectory: string, rootFiles: string[], expectedFilesCount: number, relativeNamesToCheck: string[]) { const options: CompilerOptions = { module: ModuleKind.CommonJS }; const host: CompilerHost = { @@ -530,7 +530,7 @@ export = C; }); }); - describe("Files with different casing", () => { + describe("moduleResolution:: Files with different casing", () => { let library: SourceFile; function test(files: Map, options: CompilerOptions, currentDirectory: string, useCaseSensitiveFileNames: boolean, rootFiles: string[], diagnosticCodes: number[]): void { const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); @@ -651,7 +651,7 @@ import b = require("./moduleB"); }); }); - describe("baseUrl augmented module resolution", () => { + describe("moduleResolution:: baseUrl augmented module resolution", () => { it("module resolution without path mappings/rootDirs", () => { test(/*hasDirectoryExists*/ false); @@ -1098,7 +1098,7 @@ import b = require("./moduleB"); }); }); - describe("ModuleResolutionHost.directoryExists", () => { + describe("moduleResolution:: ModuleResolutionHost.directoryExists", () => { it("No 'fileExists' calls if containing directory is missing", () => { const host: ModuleResolutionHost = { readFile: notImplemented, @@ -1111,7 +1111,7 @@ import b = require("./moduleB"); }); }); - describe("Type reference directive resolution: ", () => { + describe("moduleResolution:: Type reference directive resolution: ", () => { function testWorker(hasDirectoryExists: boolean, typesRoot: string | undefined, typeDirective: string, primary: boolean, initialFile: File, targetFile: File, ...otherFiles: File[]) { const host = createModuleResolutionHost(hasDirectoryExists, ...[initialFile, targetFile].concat(...otherFiles)); const result = resolveTypeReferenceDirective(typeDirective, initialFile.name, typesRoot ? { typeRoots: [typesRoot] } : {}, host); diff --git a/src/testRunner/unittests/programMissingFiles.ts b/src/testRunner/unittests/programApi.ts similarity index 79% rename from src/testRunner/unittests/programMissingFiles.ts rename to src/testRunner/unittests/programApi.ts index 55dfa7f5cc2..8fa107513fe 100644 --- a/src/testRunner/unittests/programMissingFiles.ts +++ b/src/testRunner/unittests/programApi.ts @@ -97,6 +97,34 @@ namespace ts { "d:/pretend/nonexistent4.tsx" ]); }); + + it("should not have missing file paths", () => { + const testSource = ` + class Foo extends HTMLElement { + bar: string = 'baz'; + }`; + + const host: CompilerHost = { + getSourceFile: (fileName: string, languageVersion: ScriptTarget, _onError?: (message: string) => void) => { + return fileName === "test.ts" ? createSourceFile(fileName, testSource, languageVersion) : undefined; + }, + getDefaultLibFileName: () => "", + writeFile: (_fileName, _content) => { throw new Error("unsupported"); }, + getCurrentDirectory: () => sys.getCurrentDirectory(), + getCanonicalFileName: fileName => sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(), + getNewLine: () => sys.newLine, + useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, + fileExists: fileName => fileName === "test.ts", + readFile: fileName => fileName === "test.ts" ? testSource : undefined, + resolveModuleNames: (_moduleNames: string[], _containingFile: string) => { throw new Error("unsupported"); }, + getDirectories: _path => { throw new Error("unsupported"); }, + }; + + const program = createProgram(["test.ts"], { module: ModuleKind.ES2015 }, host); + assert(program.getSourceFiles().length === 1, "expected 'getSourceFiles' length to be 1"); + assert(program.getMissingFilePaths().length === 0, "expected 'getMissingFilePaths' length to be 0"); + assert(program.getFileProcessingDiagnostics().getDiagnostics().length === 0, "expected 'getFileProcessingDiagnostics' length to be 0"); + }); }); describe("Program.isSourceFileFromExternalLibrary", () => { diff --git a/src/testRunner/unittests/programNoParseFalsyFileNames.ts b/src/testRunner/unittests/programNoParseFalsyFileNames.ts deleted file mode 100644 index 8040e7c7f43..00000000000 --- a/src/testRunner/unittests/programNoParseFalsyFileNames.ts +++ /dev/null @@ -1,36 +0,0 @@ -namespace ts { - describe("programNoParseFalsyFileNames", () => { - let program: Program; - - beforeEach(() => { - const testSource = ` - class Foo extends HTMLElement { - bar: string = 'baz'; - }`; - - const host: CompilerHost = { - getSourceFile: (fileName: string, languageVersion: ScriptTarget, _onError?: (message: string) => void) => { - return fileName === "test.ts" ? createSourceFile(fileName, testSource, languageVersion) : undefined; - }, - getDefaultLibFileName: () => "", - writeFile: (_fileName, _content) => { throw new Error("unsupported"); }, - getCurrentDirectory: () => sys.getCurrentDirectory(), - getCanonicalFileName: fileName => sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(), - getNewLine: () => sys.newLine, - useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, - fileExists: fileName => fileName === "test.ts", - readFile: fileName => fileName === "test.ts" ? testSource : undefined, - resolveModuleNames: (_moduleNames: string[], _containingFile: string) => { throw new Error("unsupported"); }, - getDirectories: _path => { throw new Error("unsupported"); }, - }; - - program = createProgram(["test.ts"], { module: ModuleKind.ES2015 }, host); - }); - - it("should not have missing file paths", () => { - assert(program.getSourceFiles().length === 1, "expected 'getSourceFiles' length to be 1"); - assert(program.getMissingFilePaths().length === 0, "expected 'getMissingFilePaths' length to be 0"); - assert(program.getFileProcessingDiagnostics().getDiagnostics().length === 0, "expected 'getFileProcessingDiagnostics' length to be 0"); - }); - }); -} \ No newline at end of file diff --git a/src/testRunner/unittests/projectErrors.ts b/src/testRunner/unittests/projectErrors.ts index b17494b59fb..d243e9d5ea4 100644 --- a/src/testRunner/unittests/projectErrors.ts +++ b/src/testRunner/unittests/projectErrors.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("Project errors", () => { + describe("tsserver:: Project Errors", () => { function checkProjectErrors(projectFiles: server.ProjectFilesWithTSDiagnostics, expectedErrors: ReadonlyArray): void { assert.isTrue(projectFiles !== undefined, "missing project files"); checkProjectErrorsWorker(projectFiles.projectErrors, expectedErrors); diff --git a/src/testRunner/unittests/projectReferences.ts b/src/testRunner/unittests/projectReferences.ts index 5b99e01585d..f67faa4609b 100644 --- a/src/testRunner/unittests/projectReferences.ts +++ b/src/testRunner/unittests/projectReferences.ts @@ -308,7 +308,7 @@ namespace ts { }); }); - describe("errors when a file in a composite project occurs outside the root", () => { + describe("project-references errors when a file in a composite project occurs outside the root", () => { it("Errors when a file is outside the rootdir", () => { const spec: TestSpecification = { "/alpha": { diff --git a/src/testRunner/unittests/reuseProgramStructure.ts b/src/testRunner/unittests/reuseProgramStructure.ts index 9e56b48a911..63b670912d8 100644 --- a/src/testRunner/unittests/reuseProgramStructure.ts +++ b/src/testRunner/unittests/reuseProgramStructure.ts @@ -210,7 +210,7 @@ namespace ts { checkCache("resolved type directives", program, fileName, expectedContent, f => f.resolvedTypeReferenceDirectiveNames, checkResolvedTypeDirective); } - describe("Reuse program structure", () => { + describe("Reuse program structure:: General", () => { const target = ScriptTarget.Latest; const files: NamedSourceText[] = [ { @@ -895,7 +895,7 @@ namespace ts { }); }); - describe("host is optional", () => { + describe("Reuse program structure:: host is optional", () => { it("should work if host is not provided", () => { createProgram([], {}); }); @@ -905,7 +905,7 @@ namespace ts { import createTestSystem = TestFSWithWatch.createWatchedSystem; import libFile = TestFSWithWatch.libFile; - describe("isProgramUptoDate should return true when there is no change in compiler options and", () => { + describe("Reuse program structure:: isProgramUptoDate should return true when there is no change in compiler options and", () => { function verifyProgramIsUptoDate( program: Program, newRootFileNames: string[], diff --git a/src/testRunner/unittests/session.ts b/src/testRunner/unittests/session.ts index bcbadac1d52..927b22fb475 100644 --- a/src/testRunner/unittests/session.ts +++ b/src/testRunner/unittests/session.ts @@ -35,7 +35,7 @@ namespace ts.server { } } - describe("the Session class", () => { + describe("tsserver:: Session:: General functionality", () => { let session: TestSession; let lastSent: protocol.Message; @@ -418,7 +418,7 @@ namespace ts.server { }); }); - describe("exceptions", () => { + describe("tsserver:: Session:: exceptions", () => { // Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test let oldPrepare: AnyFunction; @@ -489,7 +489,7 @@ namespace ts.server { }); }); - describe("how Session is extendable via subclassing", () => { + describe("tsserver:: Session:: how Session is extendable via subclassing", () => { class TestSession extends Session { lastSent: protocol.Message | undefined; customHandler = "testhandler"; @@ -558,7 +558,7 @@ namespace ts.server { }); }); - describe("an example of using the Session API to create an in-process server", () => { + describe("tsserver:: Session:: an example of using the Session API to create an in-process server", () => { class InProcSession extends Session { private queue: protocol.Request[] = []; constructor(private client: InProcClient) { @@ -710,7 +710,7 @@ namespace ts.server { }); }); - describe("helpers", () => { + describe("tsserver:: Session:: helpers", () => { it(getLocationInNewDocument.name, () => { const text = `// blank line\nconst x = 0;`; const renameLocationInOldText = text.indexOf("0"); diff --git a/src/testRunner/unittests/tsconfigParsing.ts b/src/testRunner/unittests/tsconfigParsing.ts index 255129d9fb6..909af429d6e 100644 --- a/src/testRunner/unittests/tsconfigParsing.ts +++ b/src/testRunner/unittests/tsconfigParsing.ts @@ -1,5 +1,5 @@ namespace ts { - describe("parseConfigFileTextToJson", () => { + describe("tsconfigParsing:: parseConfigFileTextToJson", () => { function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; error?: Diagnostic[] }) { const parsed = parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject)); diff --git a/src/testRunner/unittests/typingsInstaller.ts b/src/testRunner/unittests/typingsInstaller.ts index 7e0790b7dbb..34c5fa2a413 100644 --- a/src/testRunner/unittests/typingsInstaller.ts +++ b/src/testRunner/unittests/typingsInstaller.ts @@ -48,7 +48,7 @@ namespace ts.projectSystem { import typingsName = TI.typingsName; - describe("local module", () => { + describe("typingsInstaller:: local module", () => { it("should not be picked up", () => { const f1 = { path: "/a/app.js", @@ -87,7 +87,7 @@ namespace ts.projectSystem { }); }); - describe("typingsInstaller", () => { + describe("typingsInstaller:: General functionality", () => { it("configured projects (typings installed) 1", () => { const file1 = { path: "/a/b/app.js", @@ -1250,7 +1250,7 @@ namespace ts.projectSystem { }); }); - describe("Validate package name:", () => { + describe("typingsInstaller:: Validate package name:", () => { it("name cannot be too long", () => { let packageName = "a"; for (let i = 0; i < 8; i++) { @@ -1274,7 +1274,7 @@ namespace ts.projectSystem { }); }); - describe("Invalid package names", () => { + describe("typingsInstaller:: Invalid package names", () => { it("should not be installed", () => { const f1 = { path: "/a/b/app.js", @@ -1306,7 +1306,7 @@ namespace ts.projectSystem { }); }); - describe("discover typings", () => { + describe("typingsInstaller:: discover typings", () => { const emptySafeList = emptyMap; it("should use mappings from safe list", () => { @@ -1518,7 +1518,7 @@ namespace ts.projectSystem { }); }); - describe("telemetry events", () => { + describe("typingsInstaller:: telemetry events", () => { it("should be received", () => { const f1 = { path: "/a/app.js", @@ -1568,7 +1568,7 @@ namespace ts.projectSystem { }); }); - describe("progress notifications", () => { + describe("typingsInstaller:: progress notifications", () => { it("should be sent for success", () => { const f1 = { path: "/a/app.js", @@ -1677,7 +1677,7 @@ namespace ts.projectSystem { }); }); - describe("typing installer's npm installation command", () => { + describe("typingsInstaller:: npm installation command", () => { const npmPath = "npm", tsVersion = "2.9.0-dev.20180410"; const packageNames = ["@types/graphql@ts2.8", "@types/highlight.js@ts2.8", "@types/jest@ts2.8", "@types/mini-css-extract-plugin@ts2.8", "@types/mongoose@ts2.8", "@types/pg@ts2.8", "@types/webpack-bundle-analyzer@ts2.8", "@types/enhanced-resolve@ts2.8", "@types/eslint-plugin-prettier@ts2.8", "@types/friendly-errors-webpack-plugin@ts2.8", "@types/hammerjs@ts2.8", "@types/history@ts2.8", "@types/image-size@ts2.8", "@types/js-cookie@ts2.8", "@types/koa-compress@ts2.8", "@types/less@ts2.8", "@types/material-ui@ts2.8", "@types/mysql@ts2.8", "@types/nodemailer@ts2.8", "@types/prettier@ts2.8", "@types/query-string@ts2.8", "@types/react-places-autocomplete@ts2.8", "@types/react-router@ts2.8", "@types/react-router-config@ts2.8", "@types/react-select@ts2.8", "@types/react-transition-group@ts2.8", "@types/redux-form@ts2.8", "@types/abbrev@ts2.8", "@types/accepts@ts2.8", "@types/acorn@ts2.8", "@types/ansi-regex@ts2.8", "@types/ansi-styles@ts2.8", "@types/anymatch@ts2.8", "@types/apollo-codegen@ts2.8", "@types/are-we-there-yet@ts2.8", "@types/argparse@ts2.8", "@types/arr-union@ts2.8", "@types/array-find-index@ts2.8", "@types/array-uniq@ts2.8", "@types/array-unique@ts2.8", "@types/arrify@ts2.8", "@types/assert-plus@ts2.8", "@types/async@ts2.8", "@types/autoprefixer@ts2.8", "@types/aws4@ts2.8", "@types/babel-code-frame@ts2.8", "@types/babel-generator@ts2.8", "@types/babel-plugin-syntax-jsx@ts2.8", "@types/babel-template@ts2.8", "@types/babel-traverse@ts2.8", "@types/babel-types@ts2.8", "@types/babylon@ts2.8", "@types/base64-js@ts2.8", "@types/basic-auth@ts2.8", "@types/big.js@ts2.8", "@types/bl@ts2.8", "@types/bluebird@ts2.8", "@types/body-parser@ts2.8", "@types/bonjour@ts2.8", "@types/boom@ts2.8", "@types/brace-expansion@ts2.8", "@types/braces@ts2.8", "@types/brorand@ts2.8", "@types/browser-resolve@ts2.8", "@types/bson@ts2.8", "@types/buffer-equal@ts2.8", "@types/builtin-modules@ts2.8", "@types/bytes@ts2.8", "@types/callsites@ts2.8", "@types/camelcase@ts2.8", "@types/camelcase-keys@ts2.8", "@types/caseless@ts2.8", "@types/change-emitter@ts2.8", "@types/check-types@ts2.8", "@types/cheerio@ts2.8", "@types/chokidar@ts2.8", "@types/chownr@ts2.8", "@types/circular-json@ts2.8", "@types/classnames@ts2.8", "@types/clean-css@ts2.8", "@types/clone@ts2.8", "@types/co-body@ts2.8", "@types/color@ts2.8", "@types/color-convert@ts2.8", "@types/color-name@ts2.8", "@types/color-string@ts2.8", "@types/colors@ts2.8", "@types/combined-stream@ts2.8", "@types/common-tags@ts2.8", "@types/component-emitter@ts2.8", "@types/compressible@ts2.8", "@types/compression@ts2.8", "@types/concat-stream@ts2.8", "@types/connect-history-api-fallback@ts2.8", "@types/content-disposition@ts2.8", "@types/content-type@ts2.8", "@types/convert-source-map@ts2.8", "@types/cookie@ts2.8", "@types/cookie-signature@ts2.8", "@types/cookies@ts2.8", "@types/core-js@ts2.8", "@types/cosmiconfig@ts2.8", "@types/create-react-class@ts2.8", "@types/cross-spawn@ts2.8", "@types/cryptiles@ts2.8", "@types/css-modules-require-hook@ts2.8", "@types/dargs@ts2.8", "@types/dateformat@ts2.8", "@types/debug@ts2.8", "@types/decamelize@ts2.8", "@types/decompress@ts2.8", "@types/decompress-response@ts2.8", "@types/deep-equal@ts2.8", "@types/deep-extend@ts2.8", "@types/deepmerge@ts2.8", "@types/defined@ts2.8", "@types/del@ts2.8", "@types/depd@ts2.8", "@types/destroy@ts2.8", "@types/detect-indent@ts2.8", "@types/detect-newline@ts2.8", "@types/diff@ts2.8", "@types/doctrine@ts2.8", "@types/download@ts2.8", "@types/draft-js@ts2.8", "@types/duplexer2@ts2.8", "@types/duplexer3@ts2.8", "@types/duplexify@ts2.8", "@types/ejs@ts2.8", "@types/end-of-stream@ts2.8", "@types/entities@ts2.8", "@types/escape-html@ts2.8", "@types/escape-string-regexp@ts2.8", "@types/escodegen@ts2.8", "@types/eslint-scope@ts2.8", "@types/eslint-visitor-keys@ts2.8", "@types/esprima@ts2.8", "@types/estraverse@ts2.8", "@types/etag@ts2.8", "@types/events@ts2.8", "@types/execa@ts2.8", "@types/exenv@ts2.8", "@types/exit@ts2.8", "@types/exit-hook@ts2.8", "@types/expect@ts2.8", "@types/express@ts2.8", "@types/express-graphql@ts2.8", "@types/extend@ts2.8", "@types/extract-zip@ts2.8", "@types/fancy-log@ts2.8", "@types/fast-diff@ts2.8", "@types/fast-levenshtein@ts2.8", "@types/figures@ts2.8", "@types/file-type@ts2.8", "@types/filenamify@ts2.8", "@types/filesize@ts2.8", "@types/finalhandler@ts2.8", "@types/find-root@ts2.8", "@types/find-up@ts2.8", "@types/findup-sync@ts2.8", "@types/forever-agent@ts2.8", "@types/form-data@ts2.8", "@types/forwarded@ts2.8", "@types/fresh@ts2.8", "@types/from2@ts2.8", "@types/fs-extra@ts2.8", "@types/get-caller-file@ts2.8", "@types/get-stdin@ts2.8", "@types/get-stream@ts2.8", "@types/get-value@ts2.8", "@types/glob-base@ts2.8", "@types/glob-parent@ts2.8", "@types/glob-stream@ts2.8", "@types/globby@ts2.8", "@types/globule@ts2.8", "@types/got@ts2.8", "@types/graceful-fs@ts2.8", "@types/gulp-rename@ts2.8", "@types/gulp-sourcemaps@ts2.8", "@types/gulp-util@ts2.8", "@types/gzip-size@ts2.8", "@types/handlebars@ts2.8", "@types/has-ansi@ts2.8", "@types/hasha@ts2.8", "@types/he@ts2.8", "@types/hoek@ts2.8", "@types/html-entities@ts2.8", "@types/html-minifier@ts2.8", "@types/htmlparser2@ts2.8", "@types/http-assert@ts2.8", "@types/http-errors@ts2.8", "@types/http-proxy@ts2.8", "@types/http-proxy-middleware@ts2.8", "@types/indent-string@ts2.8", "@types/inflected@ts2.8", "@types/inherits@ts2.8", "@types/ini@ts2.8", "@types/inline-style-prefixer@ts2.8", "@types/inquirer@ts2.8", "@types/internal-ip@ts2.8", "@types/into-stream@ts2.8", "@types/invariant@ts2.8", "@types/ip@ts2.8", "@types/ip-regex@ts2.8", "@types/is-absolute-url@ts2.8", "@types/is-binary-path@ts2.8", "@types/is-finite@ts2.8", "@types/is-glob@ts2.8", "@types/is-my-json-valid@ts2.8", "@types/is-number@ts2.8", "@types/is-object@ts2.8", "@types/is-path-cwd@ts2.8", "@types/is-path-in-cwd@ts2.8", "@types/is-promise@ts2.8", "@types/is-scoped@ts2.8", "@types/is-stream@ts2.8", "@types/is-svg@ts2.8", "@types/is-url@ts2.8", "@types/is-windows@ts2.8", "@types/istanbul-lib-coverage@ts2.8", "@types/istanbul-lib-hook@ts2.8", "@types/istanbul-lib-instrument@ts2.8", "@types/istanbul-lib-report@ts2.8", "@types/istanbul-lib-source-maps@ts2.8", "@types/istanbul-reports@ts2.8", "@types/jest-diff@ts2.8", "@types/jest-docblock@ts2.8", "@types/jest-get-type@ts2.8", "@types/jest-matcher-utils@ts2.8", "@types/jest-validate@ts2.8", "@types/jpeg-js@ts2.8", "@types/js-base64@ts2.8", "@types/js-string-escape@ts2.8", "@types/js-yaml@ts2.8", "@types/jsbn@ts2.8", "@types/jsdom@ts2.8", "@types/jsesc@ts2.8", "@types/json-parse-better-errors@ts2.8", "@types/json-schema@ts2.8", "@types/json-stable-stringify@ts2.8", "@types/json-stringify-safe@ts2.8", "@types/json5@ts2.8", "@types/jsonfile@ts2.8", "@types/jsontoxml@ts2.8", "@types/jss@ts2.8", "@types/keygrip@ts2.8", "@types/keymirror@ts2.8", "@types/keyv@ts2.8", "@types/klaw@ts2.8", "@types/koa-send@ts2.8", "@types/leven@ts2.8", "@types/listr@ts2.8", "@types/load-json-file@ts2.8", "@types/loader-runner@ts2.8", "@types/loader-utils@ts2.8", "@types/locate-path@ts2.8", "@types/lodash-es@ts2.8", "@types/lodash.assign@ts2.8", "@types/lodash.camelcase@ts2.8", "@types/lodash.clonedeep@ts2.8", "@types/lodash.debounce@ts2.8", "@types/lodash.escape@ts2.8", "@types/lodash.flowright@ts2.8", "@types/lodash.get@ts2.8", "@types/lodash.isarguments@ts2.8", "@types/lodash.isarray@ts2.8", "@types/lodash.isequal@ts2.8", "@types/lodash.isobject@ts2.8", "@types/lodash.isstring@ts2.8", "@types/lodash.keys@ts2.8", "@types/lodash.memoize@ts2.8", "@types/lodash.merge@ts2.8", "@types/lodash.mergewith@ts2.8", "@types/lodash.pick@ts2.8", "@types/lodash.sortby@ts2.8", "@types/lodash.tail@ts2.8", "@types/lodash.template@ts2.8", "@types/lodash.throttle@ts2.8", "@types/lodash.unescape@ts2.8", "@types/lodash.uniq@ts2.8", "@types/log-symbols@ts2.8", "@types/log-update@ts2.8", "@types/loglevel@ts2.8", "@types/loud-rejection@ts2.8", "@types/lru-cache@ts2.8", "@types/make-dir@ts2.8", "@types/map-obj@ts2.8", "@types/media-typer@ts2.8", "@types/mem@ts2.8", "@types/mem-fs@ts2.8", "@types/memory-fs@ts2.8", "@types/meow@ts2.8", "@types/merge-descriptors@ts2.8", "@types/merge-stream@ts2.8", "@types/methods@ts2.8", "@types/micromatch@ts2.8", "@types/mime@ts2.8", "@types/mime-db@ts2.8", "@types/mime-types@ts2.8", "@types/minimatch@ts2.8", "@types/minimist@ts2.8", "@types/minipass@ts2.8", "@types/mkdirp@ts2.8", "@types/mongodb@ts2.8", "@types/morgan@ts2.8", "@types/move-concurrently@ts2.8", "@types/ms@ts2.8", "@types/msgpack-lite@ts2.8", "@types/multimatch@ts2.8", "@types/mz@ts2.8", "@types/negotiator@ts2.8", "@types/node-dir@ts2.8", "@types/node-fetch@ts2.8", "@types/node-forge@ts2.8", "@types/node-int64@ts2.8", "@types/node-ipc@ts2.8", "@types/node-notifier@ts2.8", "@types/nomnom@ts2.8", "@types/nopt@ts2.8", "@types/normalize-package-data@ts2.8", "@types/normalize-url@ts2.8", "@types/number-is-nan@ts2.8", "@types/object-assign@ts2.8", "@types/on-finished@ts2.8", "@types/on-headers@ts2.8", "@types/once@ts2.8", "@types/onetime@ts2.8", "@types/opener@ts2.8", "@types/opn@ts2.8", "@types/optimist@ts2.8", "@types/ora@ts2.8", "@types/os-homedir@ts2.8", "@types/os-locale@ts2.8", "@types/os-tmpdir@ts2.8", "@types/p-cancelable@ts2.8", "@types/p-each-series@ts2.8", "@types/p-event@ts2.8", "@types/p-lazy@ts2.8", "@types/p-limit@ts2.8", "@types/p-locate@ts2.8", "@types/p-map@ts2.8", "@types/p-map-series@ts2.8", "@types/p-reduce@ts2.8", "@types/p-timeout@ts2.8", "@types/p-try@ts2.8", "@types/pako@ts2.8", "@types/parse-glob@ts2.8", "@types/parse-json@ts2.8", "@types/parseurl@ts2.8", "@types/path-exists@ts2.8", "@types/path-is-absolute@ts2.8", "@types/path-parse@ts2.8", "@types/pg-pool@ts2.8", "@types/pg-types@ts2.8", "@types/pify@ts2.8", "@types/pixelmatch@ts2.8", "@types/pkg-dir@ts2.8", "@types/pluralize@ts2.8", "@types/pngjs@ts2.8", "@types/prelude-ls@ts2.8", "@types/pretty-bytes@ts2.8", "@types/pretty-format@ts2.8", "@types/progress@ts2.8", "@types/promise-retry@ts2.8", "@types/proxy-addr@ts2.8", "@types/pump@ts2.8", "@types/q@ts2.8", "@types/qs@ts2.8", "@types/range-parser@ts2.8", "@types/rc@ts2.8", "@types/rc-select@ts2.8", "@types/rc-slider@ts2.8", "@types/rc-tooltip@ts2.8", "@types/rc-tree@ts2.8", "@types/react-event-listener@ts2.8", "@types/react-side-effect@ts2.8", "@types/react-slick@ts2.8", "@types/read-chunk@ts2.8", "@types/read-pkg@ts2.8", "@types/read-pkg-up@ts2.8", "@types/recompose@ts2.8", "@types/recursive-readdir@ts2.8", "@types/relateurl@ts2.8", "@types/replace-ext@ts2.8", "@types/request@ts2.8", "@types/request-promise-native@ts2.8", "@types/require-directory@ts2.8", "@types/require-from-string@ts2.8", "@types/require-relative@ts2.8", "@types/resolve@ts2.8", "@types/resolve-from@ts2.8", "@types/retry@ts2.8", "@types/rx@ts2.8", "@types/rx-lite@ts2.8", "@types/rx-lite-aggregates@ts2.8", "@types/safe-regex@ts2.8", "@types/sane@ts2.8", "@types/sass-graph@ts2.8", "@types/sax@ts2.8", "@types/scriptjs@ts2.8", "@types/semver@ts2.8", "@types/send@ts2.8", "@types/serialize-javascript@ts2.8", "@types/serve-index@ts2.8", "@types/serve-static@ts2.8", "@types/set-value@ts2.8", "@types/shallowequal@ts2.8", "@types/shelljs@ts2.8", "@types/sockjs@ts2.8", "@types/sockjs-client@ts2.8", "@types/source-list-map@ts2.8", "@types/source-map-support@ts2.8", "@types/spdx-correct@ts2.8", "@types/spdy@ts2.8", "@types/split@ts2.8", "@types/sprintf@ts2.8", "@types/sprintf-js@ts2.8", "@types/sqlstring@ts2.8", "@types/sshpk@ts2.8", "@types/stack-utils@ts2.8", "@types/stat-mode@ts2.8", "@types/statuses@ts2.8", "@types/strict-uri-encode@ts2.8", "@types/string-template@ts2.8", "@types/strip-ansi@ts2.8", "@types/strip-bom@ts2.8", "@types/strip-json-comments@ts2.8", "@types/supports-color@ts2.8", "@types/svg2png@ts2.8", "@types/svgo@ts2.8", "@types/table@ts2.8", "@types/tapable@ts2.8", "@types/tar@ts2.8", "@types/temp@ts2.8", "@types/tempfile@ts2.8", "@types/through@ts2.8", "@types/through2@ts2.8", "@types/tinycolor2@ts2.8", "@types/tmp@ts2.8", "@types/to-absolute-glob@ts2.8", "@types/tough-cookie@ts2.8", "@types/trim@ts2.8", "@types/tryer@ts2.8", "@types/type-check@ts2.8", "@types/type-is@ts2.8", "@types/ua-parser-js@ts2.8", "@types/uglify-js@ts2.8", "@types/uglifyjs-webpack-plugin@ts2.8", "@types/underscore@ts2.8", "@types/uniq@ts2.8", "@types/uniqid@ts2.8", "@types/untildify@ts2.8", "@types/urijs@ts2.8", "@types/url-join@ts2.8", "@types/url-parse@ts2.8", "@types/url-regex@ts2.8", "@types/user-home@ts2.8", "@types/util-deprecate@ts2.8", "@types/util.promisify@ts2.8", "@types/utils-merge@ts2.8", "@types/uuid@ts2.8", "@types/vali-date@ts2.8", "@types/vary@ts2.8", "@types/verror@ts2.8", "@types/vinyl@ts2.8", "@types/vinyl-fs@ts2.8", "@types/warning@ts2.8", "@types/watch@ts2.8", "@types/watchpack@ts2.8", "@types/webpack-dev-middleware@ts2.8", "@types/webpack-sources@ts2.8", "@types/which@ts2.8", "@types/window-size@ts2.8", "@types/wrap-ansi@ts2.8", "@types/write-file-atomic@ts2.8", "@types/ws@ts2.8", "@types/xml2js@ts2.8", "@types/xmlbuilder@ts2.8", "@types/xtend@ts2.8", "@types/yallist@ts2.8", "@types/yargs@ts2.8", "@types/yauzl@ts2.8", "@types/yeoman-generator@ts2.8", "@types/zen-observable@ts2.8", "@types/react-content-loader@ts2.8"]; const expectedCommands = [ @@ -1705,7 +1705,7 @@ namespace ts.projectSystem { }); }); - describe("recomputing resolutions of unresolved imports", () => { + describe("typingsInstaller:: recomputing resolutions of unresolved imports", () => { const globalTypingsCacheLocation = "/tmp"; const appPath = "/a/b/app.js" as Path; const foooPath = "/a/b/node_modules/fooo/index.d.ts"; From 47200acfcd38d4febefb16b3e982dee77f6a1b2d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 09:49:57 -0800 Subject: [PATCH 02/20] Split watch environment into its own unittest --- src/testRunner/tsconfig.json | 1 + src/testRunner/unittests/tscWatchMode.ts | 179 +----------- .../unittests/tsserverProjectSystem.ts | 84 ------ src/testRunner/unittests/watchEnvironment.ts | 266 ++++++++++++++++++ 4 files changed, 268 insertions(+), 262 deletions(-) create mode 100644 src/testRunner/unittests/watchEnvironment.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 10b8637aede..bd3eded500d 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -87,6 +87,7 @@ "unittests/tsconfigParsing.ts", "unittests/tscWatchMode.ts", "unittests/versionCache.ts", + "unittests/watchEnvironment.ts", "unittests/evaluation/asyncArrow.ts", "unittests/evaluation/asyncGenerator.ts", "unittests/evaluation/forAwaitOf.ts", diff --git a/src/testRunner/unittests/tscWatchMode.ts b/src/testRunner/unittests/tscWatchMode.ts index 65f64b9da78..03bead8178b 100644 --- a/src/testRunner/unittests/tscWatchMode.ts +++ b/src/testRunner/unittests/tscWatchMode.ts @@ -10,7 +10,6 @@ namespace ts.tscWatch { export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; export import checkOutputContains = TestFSWithWatch.checkOutputContains; export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; - export import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; export function checkProgramActualFiles(program: Program, expectedFiles: ReadonlyArray) { checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); @@ -34,7 +33,7 @@ namespace ts.tscWatch { return () => watch.getCurrentProgram().getProgram(); } - function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, maxNumberOfFilesToIterateForInvalidation?: number) { + export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, maxNumberOfFilesToIterateForInvalidation?: number) { const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, host); compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; const watch = createWatchProgram(compilerHost); @@ -2885,182 +2884,6 @@ declare module "fs" { }); }); - describe("tsc-watch with different polling/non polling options", () => { - it("watchFile using dynamic priority polling", () => { - const projectFolder = "/a/username/project"; - const file1: File = { - path: `${projectFolder}/typescript.ts`, - content: "var z = 10;" - }; - const files = [file1, libFile]; - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling"); - const host = createWatchedSystem(files, { environmentVariables }); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); - - const initialProgram = watch(); - verifyProgram(); - - const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium]; - for (let index = 0; index < mediumPollingIntervalThreshold; index++) { - // Transition libFile and file1 to low priority queue - host.checkTimeoutQueueLengthAndRun(1); - assert.deepEqual(watch(), initialProgram); - } - - // Make a change to file - file1.content = "var zz30 = 100;"; - host.reloadFS(files); - - // This should detect change in the file - host.checkTimeoutQueueLengthAndRun(1); - assert.deepEqual(watch(), initialProgram); - - // Callbacks: medium priority + high priority queue and scheduled program update - host.checkTimeoutQueueLengthAndRun(3); - // During this timeout the file would be detected as unchanged - let fileUnchangeDetected = 1; - const newProgram = watch(); - assert.notStrictEqual(newProgram, initialProgram); - - verifyProgram(); - const outputFile1 = changeExtension(file1.path, ".js"); - assert.isTrue(host.fileExists(outputFile1)); - assert.equal(host.readFile(outputFile1), file1.content + host.newLine); - - const newThreshold = unchangedPollThresholds[PollingInterval.Low] + mediumPollingIntervalThreshold; - for (; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) { - // For high + Medium/low polling interval - host.checkTimeoutQueueLengthAndRun(2); - assert.deepEqual(watch(), newProgram); - } - - // Everything goes in high polling interval queue - host.checkTimeoutQueueLengthAndRun(1); - assert.deepEqual(watch(), newProgram); - - function verifyProgram() { - checkProgramActualFiles(watch(), files.map(f => f.path)); - checkWatchedFiles(host, []); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, [], /*recursive*/ true); - } - }); - - describe("tsc-watch when watchDirectories implementation", () => { - function verifyRenamingFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { - const projectFolder = "/a/username/project"; - const projectSrcFolder = `${projectFolder}/src`; - const configFile: File = { - path: `${projectFolder}/tsconfig.json`, - content: "{}" - }; - const file: File = { - path: `${projectSrcFolder}/file1.ts`, - content: "" - }; - const programFiles = [file, libFile]; - const files = [file, configFile, libFile]; - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); - const host = createWatchedSystem(files, { environmentVariables }); - const watch = createWatchOfConfigFile(configFile.path, host); - const projectFolders = [projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`]; - // Watching files config file, file, lib file - const expectedWatchedFiles = files.map(f => f.path); - const expectedWatchedDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? projectFolders : emptyArray; - if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) { - expectedWatchedFiles.push(...projectFolders); - } - - verifyProgram(checkOutputErrorsInitial); - - // Rename the file: - file.path = file.path.replace("file1.ts", "file2.ts"); - expectedWatchedFiles[0] = file.path; - host.reloadFS(files); - if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { - // With dynamic polling the fs change would be detected only by running timeouts - host.runQueuedTimeoutCallbacks(); - } - // Delayed update program - host.runQueuedTimeoutCallbacks(); - verifyProgram(checkOutputErrorsIncremental); - - function verifyProgram(checkOutputErrors: (host: WatchedSystem, errors: ReadonlyArray) => void) { - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrors(host, emptyArray); - - const outputFile = changeExtension(file.path, ".js"); - assert(host.fileExists(outputFile)); - assert.equal(host.readFile(outputFile), file.content); - - checkWatchedDirectories(host, emptyArray, /*recursive*/ true); - - // Watching config file, file, lib file and directories - checkWatchedFilesDetailed(host, expectedWatchedFiles, 1); - checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, 1, /*recursive*/ false); - } - } - - it("uses watchFile when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(Tsc_WatchDirectory.WatchFile); - }); - - it("uses non recursive watchDirectory when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory); - }); - - it("uses non recursive dynamic polling when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); - }); - - it("when there are symlinks to folders in recursive folders", () => { - const cwd = "/home/user/projects/myproject"; - const file1: File = { - path: `${cwd}/src/file.ts`, - content: `import * as a from "a"` - }; - const tsconfig: File = { - path: `${cwd}/tsconfig.json`, - content: `{ "compilerOptions": { "extendedDiagnostics": true, "traceResolution": true }}` - }; - const realA: File = { - path: `${cwd}/node_modules/reala/index.d.ts`, - content: `export {}` - }; - const realB: File = { - path: `${cwd}/node_modules/realb/index.d.ts`, - content: `export {}` - }; - const symLinkA: SymLink = { - path: `${cwd}/node_modules/a`, - symLink: `${cwd}/node_modules/reala` - }; - const symLinkB: SymLink = { - path: `${cwd}/node_modules/b`, - symLink: `${cwd}/node_modules/realb` - }; - const symLinkBInA: SymLink = { - path: `${cwd}/node_modules/reala/node_modules/b`, - symLink: `${cwd}/node_modules/b` - }; - const symLinkAInB: SymLink = { - path: `${cwd}/node_modules/realb/node_modules/a`, - symLink: `${cwd}/node_modules/a` - }; - const files = [file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB]; - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory); - const host = createWatchedSystem(files, { environmentVariables, currentDirectory: cwd }); - createWatchOfConfigFile("tsconfig.json", host); - checkWatchedDirectories(host, emptyArray, /*recursive*/ true); - checkWatchedDirectories(host, [cwd, `${cwd}/node_modules`, `${cwd}/node_modules/@types`, `${cwd}/node_modules/reala`, `${cwd}/node_modules/realb`, - `${cwd}/node_modules/reala/node_modules`, `${cwd}/node_modules/realb/node_modules`, `${cwd}/src`], /*recursive*/ false); - }); - }); - }); - describe("tsc-watch with modules linked to sibling folder", () => { const projectRoot = "/user/username/projects/project"; const mainPackageRoot = `${projectRoot}/main`; diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index e6fc1fa3726..cefa8a5e34c 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -15,7 +15,6 @@ namespace ts.projectSystem { export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; import safeList = TestFSWithWatch.safeList; - import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; function mapOutputToJson(s: string) { @@ -9156,89 +9155,6 @@ export const x = 10;` }); }); - describe("tsserverProjectSystem watchDirectories implementation", () => { - function verifyCompletionListWithNewFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { - const projectFolder = "/a/username/project"; - const projectSrcFolder = `${projectFolder}/src`; - const configFile: File = { - path: `${projectFolder}/tsconfig.json`, - content: "{}" - }; - const index: File = { - path: `${projectSrcFolder}/index.ts`, - content: `import {} from "./"` - }; - const file1: File = { - path: `${projectSrcFolder}/file1.ts`, - content: "" - }; - - const files = [index, file1, configFile, libFile]; - const fileNames = files.map(file => file.path); - // All closed files(files other than index), project folder, project/src folder and project/node_modules/@types folder - const expectedWatchedFiles = arrayToMap(fileNames.slice(1), s => s, () => 1); - const expectedWatchedDirectories = createMap(); - const mapOfDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? - expectedWatchedDirectories : - tscWatchDirectory === Tsc_WatchDirectory.WatchFile ? - expectedWatchedFiles : - createMap(); - // For failed resolution lookup and tsconfig files => cached so only watched only once - mapOfDirectories.set(projectFolder, 1); - // Through above recursive watches - mapOfDirectories.set(projectSrcFolder, 1); - // node_modules/@types folder - mapOfDirectories.set(`${projectFolder}/${nodeModulesAtTypes}`, 1); - const expectedCompletions = ["file1"]; - const completionPosition = index.content.lastIndexOf('"'); - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); - const host = createServerHost(files, { environmentVariables }); - const projectService = createProjectService(host); - projectService.openClientFile(index.path); - - const project = Debug.assertDefined(projectService.configuredProjects.get(configFile.path)); - verifyProjectAndCompletions(); - - // Add file2 - const file2: File = { - path: `${projectSrcFolder}/file2.ts`, - content: "" - }; - files.push(file2); - fileNames.push(file2.path); - expectedWatchedFiles.set(file2.path, 1); - expectedCompletions.push("file2"); - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - assert.equal(projectService.configuredProjects.get(configFile.path), project); - verifyProjectAndCompletions(); - - function verifyProjectAndCompletions() { - const completions = project.getLanguageService().getCompletionsAtPosition(index.path, completionPosition, { includeExternalModuleExports: false, includeInsertTextCompletions: false })!; - checkArray("Completion Entries", completions.entries.map(e => e.name), expectedCompletions); - - checkWatchedDirectories(host, emptyArray, /*recursive*/ true); - - checkWatchedFilesDetailed(host, expectedWatchedFiles); - checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, /*recursive*/ false); - checkProjectActualFiles(project, fileNames); - } - } - - it("uses watchFile when file is added to subfolder, completion list has new file", () => { - verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.WatchFile); - }); - - it("uses non recursive watchDirectory when file is added to subfolder, completion list has new file", () => { - verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory); - }); - - it("uses dynamic polling when file is added to subfolder, completion list has new file", () => { - verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); - }); - }); - describe("tsserverProjectSystem getEditsForFileRename", () => { it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => { const userTs: File = { diff --git a/src/testRunner/unittests/watchEnvironment.ts b/src/testRunner/unittests/watchEnvironment.ts new file mode 100644 index 00000000000..5b620b54e85 --- /dev/null +++ b/src/testRunner/unittests/watchEnvironment.ts @@ -0,0 +1,266 @@ +namespace ts { + import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; + + export namespace tscWatch { + describe("watchEnvironment:: tsc-watch with different polling/non polling options", () => { + it("watchFile using dynamic priority polling", () => { + const projectFolder = "/a/username/project"; + const file1: File = { + path: `${projectFolder}/typescript.ts`, + content: "var z = 10;" + }; + const files = [file1, libFile]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling"); + const host = createWatchedSystem(files, { environmentVariables }); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); + + const initialProgram = watch(); + verifyProgram(); + + const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium]; + for (let index = 0; index < mediumPollingIntervalThreshold; index++) { + // Transition libFile and file1 to low priority queue + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), initialProgram); + } + + // Make a change to file + file1.content = "var zz30 = 100;"; + host.reloadFS(files); + + // This should detect change in the file + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), initialProgram); + + // Callbacks: medium priority + high priority queue and scheduled program update + host.checkTimeoutQueueLengthAndRun(3); + // During this timeout the file would be detected as unchanged + let fileUnchangeDetected = 1; + const newProgram = watch(); + assert.notStrictEqual(newProgram, initialProgram); + + verifyProgram(); + const outputFile1 = changeExtension(file1.path, ".js"); + assert.isTrue(host.fileExists(outputFile1)); + assert.equal(host.readFile(outputFile1), file1.content + host.newLine); + + const newThreshold = unchangedPollThresholds[PollingInterval.Low] + mediumPollingIntervalThreshold; + for (; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) { + // For high + Medium/low polling interval + host.checkTimeoutQueueLengthAndRun(2); + assert.deepEqual(watch(), newProgram); + } + + // Everything goes in high polling interval queue + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), newProgram); + + function verifyProgram() { + checkProgramActualFiles(watch(), files.map(f => f.path)); + checkWatchedFiles(host, []); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, [], /*recursive*/ true); + } + }); + + describe("tsc-watch when watchDirectories implementation", () => { + function verifyRenamingFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: File = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const file: File = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + const programFiles = [file, libFile]; + const files = [file, configFile, libFile]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); + const host = createWatchedSystem(files, { environmentVariables }); + const watch = createWatchOfConfigFile(configFile.path, host); + const projectFolders = [projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`]; + // Watching files config file, file, lib file + const expectedWatchedFiles = files.map(f => f.path); + const expectedWatchedDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? projectFolders : emptyArray; + if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) { + expectedWatchedFiles.push(...projectFolders); + } + + verifyProgram(checkOutputErrorsInitial); + + // Rename the file: + file.path = file.path.replace("file1.ts", "file2.ts"); + expectedWatchedFiles[0] = file.path; + host.reloadFS(files); + if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { + // With dynamic polling the fs change would be detected only by running timeouts + host.runQueuedTimeoutCallbacks(); + } + // Delayed update program + host.runQueuedTimeoutCallbacks(); + verifyProgram(checkOutputErrorsIncremental); + + function verifyProgram(checkOutputErrors: (host: WatchedSystem, errors: ReadonlyArray) => void) { + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrors(host, emptyArray); + + const outputFile = changeExtension(file.path, ".js"); + assert(host.fileExists(outputFile)); + assert.equal(host.readFile(outputFile), file.content); + + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + + // Watching config file, file, lib file and directories + checkWatchedFilesDetailed(host, expectedWatchedFiles, 1); + checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, 1, /*recursive*/ false); + } + } + + it("uses watchFile when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(Tsc_WatchDirectory.WatchFile); + }); + + it("uses non recursive watchDirectory when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory); + }); + + it("uses non recursive dynamic polling when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); + }); + + it("when there are symlinks to folders in recursive folders", () => { + const cwd = "/home/user/projects/myproject"; + const file1: File = { + path: `${cwd}/src/file.ts`, + content: `import * as a from "a"` + }; + const tsconfig: File = { + path: `${cwd}/tsconfig.json`, + content: `{ "compilerOptions": { "extendedDiagnostics": true, "traceResolution": true }}` + }; + const realA: File = { + path: `${cwd}/node_modules/reala/index.d.ts`, + content: `export {}` + }; + const realB: File = { + path: `${cwd}/node_modules/realb/index.d.ts`, + content: `export {}` + }; + const symLinkA: SymLink = { + path: `${cwd}/node_modules/a`, + symLink: `${cwd}/node_modules/reala` + }; + const symLinkB: SymLink = { + path: `${cwd}/node_modules/b`, + symLink: `${cwd}/node_modules/realb` + }; + const symLinkBInA: SymLink = { + path: `${cwd}/node_modules/reala/node_modules/b`, + symLink: `${cwd}/node_modules/b` + }; + const symLinkAInB: SymLink = { + path: `${cwd}/node_modules/realb/node_modules/a`, + symLink: `${cwd}/node_modules/a` + }; + const files = [file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory); + const host = createWatchedSystem(files, { environmentVariables, currentDirectory: cwd }); + createWatchOfConfigFile("tsconfig.json", host); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + checkWatchedDirectories(host, [cwd, `${cwd}/node_modules`, `${cwd}/node_modules/@types`, `${cwd}/node_modules/reala`, `${cwd}/node_modules/realb`, + `${cwd}/node_modules/reala/node_modules`, `${cwd}/node_modules/realb/node_modules`, `${cwd}/src`], /*recursive*/ false); + }); + }); + }); + } + + export namespace projectSystem { + describe("watchEnvironment:: tsserverProjectSystem watchDirectories implementation", () => { + function verifyCompletionListWithNewFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: File = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const index: File = { + path: `${projectSrcFolder}/index.ts`, + content: `import {} from "./"` + }; + const file1: File = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + + const files = [index, file1, configFile, libFile]; + const fileNames = files.map(file => file.path); + // All closed files(files other than index), project folder, project/src folder and project/node_modules/@types folder + const expectedWatchedFiles = arrayToMap(fileNames.slice(1), s => s, () => 1); + const expectedWatchedDirectories = createMap(); + const mapOfDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? + expectedWatchedDirectories : + tscWatchDirectory === Tsc_WatchDirectory.WatchFile ? + expectedWatchedFiles : + createMap(); + // For failed resolution lookup and tsconfig files => cached so only watched only once + mapOfDirectories.set(projectFolder, 1); + // Through above recursive watches + mapOfDirectories.set(projectSrcFolder, 1); + // node_modules/@types folder + mapOfDirectories.set(`${projectFolder}/${nodeModulesAtTypes}`, 1); + const expectedCompletions = ["file1"]; + const completionPosition = index.content.lastIndexOf('"'); + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); + const host = createServerHost(files, { environmentVariables }); + const projectService = createProjectService(host); + projectService.openClientFile(index.path); + + const project = Debug.assertDefined(projectService.configuredProjects.get(configFile.path)); + verifyProjectAndCompletions(); + + // Add file2 + const file2: File = { + path: `${projectSrcFolder}/file2.ts`, + content: "" + }; + files.push(file2); + fileNames.push(file2.path); + expectedWatchedFiles.set(file2.path, 1); + expectedCompletions.push("file2"); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + assert.equal(projectService.configuredProjects.get(configFile.path), project); + verifyProjectAndCompletions(); + + function verifyProjectAndCompletions() { + const completions = project.getLanguageService().getCompletionsAtPosition(index.path, completionPosition, { includeExternalModuleExports: false, includeInsertTextCompletions: false })!; + checkArray("Completion Entries", completions.entries.map(e => e.name), expectedCompletions); + + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + + checkWatchedFilesDetailed(host, expectedWatchedFiles); + checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, /*recursive*/ false); + checkProjectActualFiles(project, fileNames); + } + } + + it("uses watchFile when file is added to subfolder, completion list has new file", () => { + verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.WatchFile); + }); + + it("uses non recursive watchDirectory when file is added to subfolder, completion list has new file", () => { + verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory); + }); + + it("uses dynamic polling when file is added to subfolder, completion list has new file", () => { + verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); + }); + }); + } +} From 7b9b0f8da71ded5a005f581f16b2d768c49ddd4b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 10:30:10 -0800 Subject: [PATCH 03/20] Split resolutionCache and watchApi tests into its own unittest --- src/testRunner/tsconfig.json | 8 +- src/testRunner/unittests/resolutionCache.ts | 981 +++++++++++++++ src/testRunner/unittests/tsbuildWatchMode.ts | 1 - src/testRunner/unittests/tscWatchHelpers.ts | 237 ++++ src/testRunner/unittests/tscWatchMode.ts | 645 ---------- src/testRunner/unittests/tsserverHelpers.ts | 679 +++++++++++ .../unittests/tsserverProjectSystem.ts | 1065 ----------------- src/testRunner/unittests/typingsInstaller.ts | 1 - src/testRunner/unittests/watchApi.ts | 40 + src/testRunner/unittests/watchEnvironment.ts | 50 + 10 files changed, 1993 insertions(+), 1714 deletions(-) create mode 100644 src/testRunner/unittests/resolutionCache.ts create mode 100644 src/testRunner/unittests/tscWatchHelpers.ts create mode 100644 src/testRunner/unittests/tsserverHelpers.ts create mode 100644 src/testRunner/unittests/watchApi.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index bd3eded500d..674188eee63 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -37,8 +37,8 @@ "runner.ts", "unittests/extractTestHelpers.ts", - "unittests/tsserverProjectSystem.ts", - "unittests/typingsInstaller.ts", + "unittests/tscWatchHelpers.ts", + "unittests/tsserverHelpers.ts", "unittests/asserts.ts", "unittests/base64.ts", @@ -72,6 +72,7 @@ "unittests/projectErrors.ts", "unittests/projectReferences.ts", "unittests/publicApi.ts", + "unittests/resolutionCache.ts", "unittests/reuseProgramStructure.ts", "unittests/session.ts", "unittests/semver.ts", @@ -86,8 +87,11 @@ "unittests/tsbuildWatchMode.ts", "unittests/tsconfigParsing.ts", "unittests/tscWatchMode.ts", + "unittests/tsserverProjectSystem.ts", + "unittests/typingsInstaller.ts", "unittests/versionCache.ts", "unittests/watchEnvironment.ts", + "unittests/watchApi.ts", "unittests/evaluation/asyncArrow.ts", "unittests/evaluation/asyncGenerator.ts", "unittests/evaluation/forAwaitOf.ts", diff --git a/src/testRunner/unittests/resolutionCache.ts b/src/testRunner/unittests/resolutionCache.ts new file mode 100644 index 00000000000..fbc792481fc --- /dev/null +++ b/src/testRunner/unittests/resolutionCache.ts @@ -0,0 +1,981 @@ +namespace ts.tscWatch { + describe("resolutionCache:: tsc-watch module resolution caching", () => { + it("works", () => { + const root = { + path: "/a/d/f0.ts", + content: `import {x} from "f1"` + }; + const imported = { + path: "/a/f1.ts", + content: `foo()` + }; + + const files = [root, imported, libFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); + const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); + + // ensure that imported file was found + checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); + + const originalFileExists = host.fileExists; + { + const newContent = `import {x} from "f1" + var x: string = 1;`; + root.content = newContent; + host.reloadFS(files); + + // patch fileExists to make sure that disk is not touched + host.fileExists = notImplemented; + + // trigger synchronization to make sure that import will be fetched from the cache + host.runQueuedTimeoutCallbacks(); + + // ensure file has correct number of errors after edit + checkOutputErrorsIncremental(host, [ + f1IsNotModule, + getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), + cannotFindFoo + ]); + } + { + let fileExistsIsCalled = false; + host.fileExists = (fileName): boolean => { + if (fileName === "lib.d.ts") { + return false; + } + fileExistsIsCalled = true; + assert.isTrue(fileName.indexOf("/f2.") !== -1); + return originalFileExists.call(host, fileName); + }; + + root.content = `import {x} from "f2"`; + host.reloadFS(files); + + // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + host.runQueuedTimeoutCallbacks(); + + // ensure file has correct number of errors after edit + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") + ]); + + assert.isTrue(fileExistsIsCalled); + } + { + let fileExistsCalled = false; + host.fileExists = (fileName): boolean => { + if (fileName === "lib.d.ts") { + return false; + } + fileExistsCalled = true; + assert.isTrue(fileName.indexOf("/f1.") !== -1); + return originalFileExists.call(host, fileName); + }; + + const newContent = `import {x} from "f1"`; + root.content = newContent; + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); + assert.isTrue(fileExistsCalled); + } + }); + + it("loads missing files from disk", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;` + }; + + const files = [root, libFile]; + const host = createWatchedSystem(files); + const originalFileExists = host.fileExists; + + let fileExistsCalledForBar = false; + host.fileExists = fileName => { + if (fileName === "lib.d.ts") { + return false; + } + if (!fileExistsCalledForBar) { + fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + } + + return originalFileExists.call(host, fileName); + }; + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") + ]); + + fileExistsCalledForBar = false; + root.content = `import {y} from "bar"`; + host.reloadFS(files.concat(imported)); + + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }); + + it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;export const x = 10;` + }; + + const files = [root, libFile]; + const filesWithImported = files.concat(imported); + const host = createWatchedSystem(filesWithImported); + const originalFileExists = host.fileExists; + let fileExistsCalledForBar = false; + host.fileExists = fileName => { + if (fileName === "lib.d.ts") { + return false; + } + if (!fileExistsCalledForBar) { + fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + } + return originalFileExists.call(host, fileName); + }; + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputErrorsInitial(host, emptyArray); + + fileExistsCalledForBar = false; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") + ]); + + fileExistsCalledForBar = false; + host.reloadFS(filesWithImported); + host.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(host, emptyArray); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }); + + it("works when module resolution changes to ambient module", () => { + const root = { + path: "/a/b/foo.ts", + content: `import * as fs from "fs";` + }; + + const packageJson = { + path: "/a/b/node_modules/@types/node/package.json", + content: ` +{ + "main": "" +} +` + }; + + const nodeType = { + path: "/a/b/node_modules/@types/node/index.d.ts", + content: ` +declare module "fs" { + export interface Stats { + isFile(): boolean; + } +}` + }; + + const files = [root, libFile]; + const filesWithNodeType = files.concat(packageJson, nodeType); + const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { }); + + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") + ]); + + host.reloadFS(filesWithNodeType); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("works when included file with ambient module changes", () => { + const root = { + path: "/a/b/foo.ts", + content: ` +import * as fs from "fs"; +import * as u from "url"; +` + }; + + const file = { + path: "/a/b/bar.d.ts", + content: ` +declare module "url" { + export interface Url { + href?: string; + } +} +` + }; + + const fileContentWithFS = ` +declare module "fs" { + export interface Stats { + isFile(): boolean; + } +} +`; + + const files = [root, file, libFile]; + const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); + + const watch = createWatchOfFilesAndCompilerOptions([root.path, file.path], host, {}); + + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") + ]); + + file.content += fileContentWithFS; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("works when reusing program with files from external library", () => { + interface ExpectedFile { path: string; isExpectedToEmit?: boolean; content?: string; } + const configDir = "/a/b/projects/myProject/src/"; + const file1: File = { + path: configDir + "file1.ts", + content: 'import module1 = require("module1");\nmodule1("hello");' + }; + const file2: File = { + path: configDir + "file2.ts", + content: 'import module11 = require("module1");\nmodule11("hello");' + }; + const module1: File = { + path: "/a/b/projects/myProject/node_modules/module1/index.js", + content: "module.exports = options => { return options.toString(); }" + }; + const configFile: File = { + path: configDir + "tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + allowJs: true, + rootDir: ".", + outDir: "../dist", + moduleResolution: "node", + maxNodeModuleJsDepth: 1 + } + }) + }; + const outDirFolder = "/a/b/projects/myProject/dist/"; + const programFiles = [file1, file2, module1, libFile]; + const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" }); + const watch = createWatchOfConfigFile(configFile.path, host); + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrorsInitial(host, emptyArray); + const expectedFiles: ExpectedFile[] = [ + createExpectedEmittedFile(file1), + createExpectedEmittedFile(file2), + createExpectedToNotEmitFile("index.js"), + createExpectedToNotEmitFile("src/index.js"), + createExpectedToNotEmitFile("src/file1.js"), + createExpectedToNotEmitFile("src/file2.js"), + createExpectedToNotEmitFile("lib.js"), + createExpectedToNotEmitFile("lib.d.ts") + ]; + verifyExpectedFiles(expectedFiles); + + file1.content += "\n;"; + expectedFiles[0].content += ";\n"; // Only emit file1 with this change + expectedFiles[1].isExpectedToEmit = false; + host.reloadFS(programFiles.concat(configFile)); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrorsIncremental(host, emptyArray); + verifyExpectedFiles(expectedFiles); + + + function verifyExpectedFiles(expectedFiles: ExpectedFile[]) { + forEach(expectedFiles, f => { + assert.equal(!!host.fileExists(f.path), f.isExpectedToEmit, "File " + f.path + " is expected to " + (f.isExpectedToEmit ? "emit" : "not emit")); + if (f.isExpectedToEmit) { + assert.equal(host.readFile(f.path), f.content, "Expected contents of " + f.path); + } + }); + } + + function createExpectedToNotEmitFile(fileName: string): ExpectedFile { + return { + path: outDirFolder + fileName, + isExpectedToEmit: false + }; + } + + function createExpectedEmittedFile(file: File): ExpectedFile { + return { + path: removeFileExtension(file.path.replace(configDir, outDirFolder)) + Extension.Js, + isExpectedToEmit: true, + content: '"use strict";\nexports.__esModule = true;\n' + file.content.replace("import", "var") + "\n" + }; + } + }); + + it("works when renaming node_modules folder that already contains @types folder", () => { + const currentDirectory = "/user/username/projects/myproject"; + const file: File = { + path: `${currentDirectory}/a.ts`, + content: `import * as q from "qqq";` + }; + const module: File = { + path: `${currentDirectory}/node_modules2/@types/qqq/index.d.ts`, + content: "export {}" + }; + const files = [file, module, libFile]; + const host = createWatchedSystem(files, { currentDirectory }); + const watch = createWatchOfFilesAndCompilerOptions([file.path], host); + + checkProgramActualFiles(watch(), [file.path, libFile.path]); + checkOutputErrorsInitial(host, [getDiagnosticModuleNotFoundOfFile(watch(), file, "qqq")]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, [`${currentDirectory}/node_modules`, `${currentDirectory}/node_modules/@types`], /*recursive*/ true); + + host.renameFolder(`${currentDirectory}/node_modules2`, `${currentDirectory}/node_modules`); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), [file.path, libFile.path, `${currentDirectory}/node_modules/@types/qqq/index.d.ts`]); + checkOutputErrorsIncremental(host, emptyArray); + }); + + describe("ignores files/folder changes in node_modules that start with '.'", () => { + const projectPath = "/user/username/projects/project"; + const npmCacheFile: File = { + path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, + content: JSON.stringify({ something: 10 }) + }; + const file1: File = { + path: `${projectPath}/test.ts`, + content: `import { x } from "somemodule";` + }; + const file2: File = { + path: `${projectPath}/node_modules/somemodule/index.d.ts`, + content: `export const x = 10;` + }; + const files = [libFile, file1, file2]; + const expectedFiles = files.map(f => f.path); + it("when watching node_modules in inferred project for failed lookup", () => { + const host = createWatchedSystem(files); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host, {}, /*maxNumberOfFilesToIterateForInvalidation*/ 1); + checkProgramActualFiles(watch(), expectedFiles); + host.checkTimeoutQueueLength(0); + + host.ensureFileOrFolder(npmCacheFile); + host.checkTimeoutQueueLength(0); + }); + it("when watching node_modules as part of wild card directories in config project", () => { + const config: File = { + path: `${projectPath}/tsconfig.json`, + content: "{}" + }; + const host = createWatchedSystem(files.concat(config)); + const watch = createWatchOfConfigFile(config.path, host); + checkProgramActualFiles(watch(), expectedFiles); + host.checkTimeoutQueueLength(0); + + host.ensureFileOrFolder(npmCacheFile); + host.checkTimeoutQueueLength(0); + }); + }); + }); + + describe("resolutionCache:: tsc-watch with modules linked to sibling folder", () => { + const projectRoot = "/user/username/projects/project"; + const mainPackageRoot = `${projectRoot}/main`; + const linkedPackageRoot = `${projectRoot}/linked-package`; + const mainFile: File = { + path: `${mainPackageRoot}/index.ts`, + content: "import { Foo } from '@scoped/linked-package'" + }; + const config: File = { + path: `${mainPackageRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, + files: ["index.ts"] + }) + }; + const linkedPackageInMain: SymLink = { + path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, + symLink: `${linkedPackageRoot}` + }; + const linkedPackageJson: File = { + path: `${linkedPackageRoot}/package.json`, + content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) + }; + const linkedPackageIndex: File = { + path: `${linkedPackageRoot}/dist/index.d.ts`, + content: "export * from './other';" + }; + const linkedPackageOther: File = { + path: `${linkedPackageRoot}/dist/other.d.ts`, + content: 'export declare const Foo = "BAR";' + }; + + it("verify watched directories", () => { + const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; + const host = createWatchedSystem(files, { currentDirectory: mainPackageRoot }); + createWatchOfConfigFile("tsconfig.json", host); + checkWatchedFilesDetailed(host, [libFile.path, mainFile.path, config.path, linkedPackageIndex.path, linkedPackageOther.path], 1); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, [`${mainPackageRoot}/@scoped`, `${mainPackageRoot}/node_modules`, linkedPackageRoot, `${mainPackageRoot}/node_modules/@types`, `${projectRoot}/node_modules/@types`], 1, /*recursive*/ true); + }); + }); +} + +namespace ts.projectSystem { + function createHostModuleResolutionTrace(host: TestServerHost & ModuleResolutionHost) { + const resolutionTrace: string[] = []; + host.trace = resolutionTrace.push.bind(resolutionTrace); + return resolutionTrace; + } + + describe("resolutionCache:: tsserverProjectSystem extra resolution pass in lshost", () => { + it("can load typings that are proper modules", () => { + const file1 = { + path: "/a/b/app.js", + content: `var x = require("lib")` + }; + const lib = { + path: "/a/cache/node_modules/@types/lib/index.d.ts", + content: "export let x = 1" + }; + const host: TestServerHost & ModuleResolutionHost = createServerHost([file1, lib]); + const resolutionTrace = createHostModuleResolutionTrace(host); + const projectService = createProjectService(host, { typingsInstaller: new TestTypingsInstaller("/a/cache", /*throttleLimit*/5, host) }); + + projectService.setCompilerOptionsForInferredProjects({ traceResolution: true, allowJs: true }); + projectService.openClientFile(file1.path); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + const proj = projectService.inferredProjects[0]; + + assert.deepEqual(resolutionTrace, [ + "======== Resolving module 'lib' from '/a/b/app.js'. ========", + "Module resolution kind is not specified, using 'NodeJs'.", + "Loading module 'lib' from 'node_modules' folder, target file type 'TypeScript'.", + "Directory '/a/b/node_modules' does not exist, skipping all lookups in it.", + "Directory '/a/node_modules' does not exist, skipping all lookups in it.", + "Directory '/node_modules' does not exist, skipping all lookups in it.", + "Loading module 'lib' from 'node_modules' folder, target file type 'JavaScript'.", + "Directory '/a/b/node_modules' does not exist, skipping all lookups in it.", + "Directory '/a/node_modules' does not exist, skipping all lookups in it.", + "Directory '/node_modules' does not exist, skipping all lookups in it.", + "======== Module name 'lib' was not resolved. ========", + `Auto discovery for typings is enabled in project '${proj.getProjectName()}'. Running extra resolution pass for module 'lib' using cache location '/a/cache'.`, + "File '/a/cache/node_modules/lib.d.ts' does not exist.", + "File '/a/cache/node_modules/@types/lib/package.json' does not exist.", + "File '/a/cache/node_modules/@types/lib.d.ts' does not exist.", + "File '/a/cache/node_modules/@types/lib/index.d.ts' exist - use it as a name resolution result.", + ]); + checkProjectActualFiles(proj, [file1.path, lib.path]); + }); + }); + + describe("resolutionCache:: tsserverProjectSystem module resolution caching", () => { + const projectLocation = "/user/username/projects/myproject"; + const configFile: File = { + path: `${projectLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { traceResolution: true } }) + }; + + function getModules(module1Path: string, module2Path: string) { + const module1: File = { + path: module1Path, + content: `export function module1() {}` + }; + const module2: File = { + 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: File, 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: File, moduleName: string, useNodeModules: boolean, cacheLocation?: string) { + let foundModule = false; + forEachAncestorDirectory(dirPath, dirPath => { + if (dirPath === cacheLocation) { + return foundModule; + } + + 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: File, moduleName: string) { + expectedTrace.push( + `======== Resolving module '${moduleName}' from '${file.path}'. ========`, + `Module resolution kind is not specified, using 'NodeJs'.` + ); + } + + function getExpectedResolutionTraceFooter(expectedTrace: string[], module: File, moduleName: string, addRealPathTrace: boolean, ignoreModuleFileFound?: boolean) { + if (!ignoreModuleFileFound) { + 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: File, module: File, 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: File, module: File, 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 getExpectedNonRelativeModuleResolutionFromCacheTrace(host: TestServerHost, file: File, module: File, moduleName: string, cacheLocation: 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, cacheLocation); + expectedTrace.push(`Resolution for module '${moduleName}' was found in cache from location '${cacheLocation}'.`); + getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ false, /*ignoreModuleFileFound*/ true); + return expectedTrace; + } + + function getExpectedReusingResolutionFromOldProgram(file: File, moduleName: string) { + return `Reusing resolution of module '${moduleName}' to file '${file.path}' from old program.`; + } + + function verifyWatchesWithConfigFile(host: TestServerHost, files: File[], openFile: File, extraExpectedDirectories?: ReadonlyArray) { + const expectedRecursiveDirectories = arrayToSet([projectLocation, `${projectLocation}/${nodeModulesAtTypes}`, ...(extraExpectedDirectories || emptyArray)]); + checkWatchedFiles(host, mapDefined(files, f => { + if (f === openFile) { + return undefined; + } + const indexOfNodeModules = f.path.indexOf("/node_modules/"); + if (indexOfNodeModules === -1) { + return f.path; + } + expectedRecursiveDirectories.set(f.path.substr(0, indexOfNodeModules + "/node_modules".length), true); + return undefined; + })); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, arrayFrom(expectedRecursiveDirectories.keys()), /*recursive*/ true); + } + + describe("from files in same folder", () => { + function getFiles(fileContent: string) { + const file1: File = { + path: `${projectLocation}/src/file1.ts`, + content: fileContent + }; + const file2: File = { + 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 expectedNonRelativeDirectories = [`${projectLocation}/node_modules`, `${projectLocation}/src`]; + 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, expectedNonRelativeDirectories); + + file1.content += fileContent; + file2.content += fileContent; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyTrace(resolutionTrace, [ + getExpectedReusingResolutionFromOldProgram(file1, module1Name), + getExpectedReusingResolutionFromOldProgram(file1, module2Name) + ]); + verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); + }); + }); + + describe("from files in different folders", () => { + function getFiles(fileContent1: string, fileContent2 = fileContent1, fileContent3 = fileContent1, fileContent4 = fileContent1) { + const file1: File = { + path: `${projectLocation}/product/src/file1.ts`, + content: fileContent1 + }; + const file2: File = { + path: `${projectLocation}/product/src/feature/file2.ts`, + content: fileContent2 + }; + const file3: File = { + path: `${projectLocation}/product/test/src/file3.ts`, + content: fileContent3 + }; + const file4: File = { + 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 expectedNonRelativeDirectories = [`${projectLocation}/node_modules`, `${projectLocation}/product`]; + 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); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module1, module1Name, getDirectoryPath(file1.path), expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module2, module2Name, getDirectoryPath(file1.path), expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module1, module1Name, `${projectLocation}/product`, expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module2, module2Name, `${projectLocation}/product`, expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module1, module1Name, getDirectoryPath(file4.path), expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module2, module2Name, getDirectoryPath(file4.path), expectedTrace); + verifyTrace(resolutionTrace, expectedTrace); + verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); + + 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, expectedNonRelativeDirectories); + }); + + 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); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module1, module1Name, getDirectoryPath(file1.path), expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module2, module2Name, getDirectoryPath(file1.path), expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module1, module1Name, `${projectLocation}/product`, expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module2, module2Name, `${projectLocation}/product`, expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module1, module1Name, getDirectoryPath(file4.path), expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module2, module2Name, getDirectoryPath(file4.path), expectedTrace); + verifyTrace(resolutionTrace, expectedTrace); + + const currentDirectory = getDirectoryPath(file1.path); + const watchedFiles = mapDefined(files, f => f === file1 || f.path.indexOf("/node_modules/") !== -1 ? undefined : f.path); + forEachAncestorDirectory(currentDirectory, d => { + watchedFiles.push(combinePaths(d, "tsconfig.json"), combinePaths(d, "jsconfig.json")); + }); + const watchedRecursiveDirectories = getTypeRootsFromLocation(currentDirectory).concat([ + `${currentDirectory}/node_modules`, `${currentDirectory}/feature`, `${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); + } + }); + }); + + describe("when watching directories for failed lookup locations in amd resolution", () => { + const projectRoot = "/user/username/projects/project"; + const nodeFile: File = { + path: `${projectRoot}/src/typings/node.d.ts`, + content: ` +declare module "fs" { + export interface something { + } +}` + }; + const electronFile: File = { + path: `${projectRoot}/src/typings/electron.d.ts`, + content: ` +declare module 'original-fs' { + import * as fs from 'fs'; + export = fs; +}` + }; + const srcFile: File = { + path: `${projectRoot}/src/somefolder/srcfile.ts`, + content: ` +import { x } from "somefolder/module1"; +import { x } from "somefolder/module2"; +const y = x;` + }; + const moduleFile: File = { + path: `${projectRoot}/src/somefolder/module1.ts`, + content: ` +export const x = 10;` + }; + const configFile: File = { + path: `${projectRoot}/src/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "amd", + moduleResolution: "classic", + target: "es5", + outDir: "../out", + baseUrl: "./", + typeRoots: ["typings"] + } + }) + }; + + function verifyModuleResolution(useNodeFile: boolean) { + const files = [...(useNodeFile ? [nodeFile] : []), electronFile, srcFile, moduleFile, configFile, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(srcFile.path, srcFile.content, ScriptKind.TS, projectRoot); + checkProjectActualFiles(service.configuredProjects.get(configFile.path)!, files.map(f => f.path)); + checkWatchedFilesDetailed(host, mapDefined(files, f => f === srcFile ? undefined : f.path), 1); + if (useNodeFile) { + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); // since fs resolves to ambient module, shouldnt watch failed lookup + } + else { + checkWatchedDirectoriesDetailed(host, [`${projectRoot}`, `${projectRoot}/src`], 1, /*recursive*/ false); // failed lookup for fs + } + const expectedWatchedDirectories = createMap(); + expectedWatchedDirectories.set(`${projectRoot}/src`, 1); // Wild card + expectedWatchedDirectories.set(`${projectRoot}/src/somefolder`, 1); // failedLookup for somefolder/module2 + expectedWatchedDirectories.set(`${projectRoot}/src/node_modules`, 1); // failed lookup for somefolder/module2 + expectedWatchedDirectories.set(`${projectRoot}/somefolder`, 1); // failed lookup for somefolder/module2 + expectedWatchedDirectories.set(`${projectRoot}/node_modules`, 1); // failed lookup for with node_modules/@types/fs + expectedWatchedDirectories.set(`${projectRoot}/src/typings`, useNodeFile ? 1 : 2); // typeroot directory + failed lookup if not using node file + checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, /*recursive*/ true); + } + + it("when resolves to ambient module", () => { + verifyModuleResolution(/*useNodeFile*/ true); + }); + + it("when resolution fails", () => { + verifyModuleResolution(/*useNodeFile*/ false); + }); + }); + + describe("ignores files/folder changes in node_modules that start with '.'", () => { + const projectPath = "/user/username/projects/project"; + const npmCacheFile: File = { + path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, + content: JSON.stringify({ something: 10 }) + }; + const file1: File = { + path: `${projectPath}/test.ts`, + content: `import { x } from "somemodule";` + }; + const file2: File = { + path: `${projectPath}/node_modules/somemodule/index.d.ts`, + content: `export const x = 10;` + }; + it("when watching node_modules in inferred project for failed lookup/closed script infos", () => { + const files = [libFile, file1, file2]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(file1.path); + checkNumberOfProjects(service, { inferredProjects: 1 }); + const project = service.inferredProjects[0]; + checkProjectActualFiles(project, files.map(f => f.path)); + (project as ResolutionCacheHost).maxNumberOfFilesToIterateForInvalidation = 1; + host.checkTimeoutQueueLength(0); + + host.ensureFileOrFolder(npmCacheFile); + host.checkTimeoutQueueLength(0); + }); + it("when watching node_modules as part of wild card directories in config project", () => { + const config: File = { + path: `${projectPath}/tsconfig.json`, + content: "{}" + }; + const files = [libFile, file1, file2, config]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(file1.path); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const project = Debug.assertDefined(service.configuredProjects.get(config.path)); + checkProjectActualFiles(project, files.map(f => f.path)); + host.checkTimeoutQueueLength(0); + + host.ensureFileOrFolder(npmCacheFile); + host.checkTimeoutQueueLength(0); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index 9178416373c..9f45da51656 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -1,5 +1,4 @@ namespace ts.tscWatch { - export import libFile = TestFSWithWatch.libFile; import projectsLocation = TestFSWithWatch.tsbuildProjectsLocation; import getFilePathInProject = TestFSWithWatch.getTsBuildProjectFilePath; import getFileFromProject = TestFSWithWatch.getTsBuildProjectFile; diff --git a/src/testRunner/unittests/tscWatchHelpers.ts b/src/testRunner/unittests/tscWatchHelpers.ts new file mode 100644 index 00000000000..6e3fd40c495 --- /dev/null +++ b/src/testRunner/unittests/tscWatchHelpers.ts @@ -0,0 +1,237 @@ +namespace ts.tscWatch { + export import WatchedSystem = TestFSWithWatch.TestServerHost; + export type File = TestFSWithWatch.File; + export type SymLink = TestFSWithWatch.SymLink; + export import libFile = TestFSWithWatch.libFile; + export import createWatchedSystem = TestFSWithWatch.createWatchedSystem; + export import checkArray = TestFSWithWatch.checkArray; + export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; + export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; + export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; + export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; + export import checkOutputContains = TestFSWithWatch.checkOutputContains; + export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; + + export function checkProgramActualFiles(program: Program, expectedFiles: ReadonlyArray) { + checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); + } + + export function checkProgramRootFiles(program: Program, expectedFiles: ReadonlyArray) { + checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); + } + + export function createWatchOfConfigFileReturningBuilder(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host); + compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; + const watch = createWatchProgram(compilerHost); + return () => watch.getCurrentProgram(); + } + + export function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host); + compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; + const watch = createWatchProgram(compilerHost); + return () => watch.getCurrentProgram().getProgram(); + } + + export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, host); + compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; + const watch = createWatchProgram(compilerHost); + return () => watch.getCurrentProgram().getProgram(); + } + + //function getEmittedLineForMultiFileOutput(file: File, host: WatchedSystem) { + // return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; + //} + + //function getEmittedLineForSingleFileOutput(filename: string, host: WatchedSystem) { + // return `TSFILE: ${filename}${host.newLine}`; + //} + + //interface FileOrFolderEmit extends File { + // output?: string; + //} + + //function getFileOrFolderEmit(file: File, getOutput?: (file: File) => string): FileOrFolderEmit { + // const result = file as FileOrFolderEmit; + // if (getOutput) { + // result.output = getOutput(file); + // } + // return result; + //} + + //function getEmittedLines(files: FileOrFolderEmit[]) { + // const seen = createMap(); + // const result: string[] = []; + // for (const { output } of files) { + // if (output && !seen.has(output)) { + // seen.set(output, true); + // result.push(output); + // } + // } + // return result; + //} + + //function checkAffectedLines(host: WatchedSystem, affectedFiles: FileOrFolderEmit[], allEmittedFiles: string[]) { + // const expectedAffectedFiles = getEmittedLines(affectedFiles); + // const expectedNonAffectedFiles = mapDefined(allEmittedFiles, line => contains(expectedAffectedFiles, line) ? undefined : line); + // checkOutputContains(host, expectedAffectedFiles); + // checkOutputDoesNotContain(host, expectedNonAffectedFiles); + //} + + const elapsedRegex = /^Elapsed:: [0-9]+ms/; + function checkOutputErrors( + host: WatchedSystem, + logsBeforeWatchDiagnostic: string[] | undefined, + preErrorsWatchDiagnostic: Diagnostic, + logsBeforeErrors: string[] | undefined, + errors: ReadonlyArray | ReadonlyArray, + disableConsoleClears?: boolean | undefined, + ...postErrorsWatchDiagnostics: Diagnostic[] + ) { + let screenClears = 0; + const outputs = host.getOutput(); + const expectedOutputCount = 1 + errors.length + postErrorsWatchDiagnostics.length + + (logsBeforeWatchDiagnostic ? logsBeforeWatchDiagnostic.length : 0) + (logsBeforeErrors ? logsBeforeErrors.length : 0); + assert.equal(outputs.length, expectedOutputCount, JSON.stringify(outputs)); + let index = 0; + forEach(logsBeforeWatchDiagnostic, log => assertLog("logsBeforeWatchDiagnostic", log)); + assertWatchDiagnostic(preErrorsWatchDiagnostic); + forEach(logsBeforeErrors, log => assertLog("logBeforeError", log)); + // Verify errors + forEach(errors, assertDiagnostic); + forEach(postErrorsWatchDiagnostics, assertWatchDiagnostic); + assert.equal(host.screenClears.length, screenClears, "Expected number of screen clears"); + host.clearOutput(); + + function isDiagnostic(diagnostic: Diagnostic | string): diagnostic is Diagnostic { + return !!(diagnostic as Diagnostic).messageText; + } + + function assertDiagnostic(diagnostic: Diagnostic | string) { + const expected = isDiagnostic(diagnostic) ? formatDiagnostic(diagnostic, host) : diagnostic; + assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected)); + index++; + } + + function assertLog(caption: string, expected: string) { + const actual = outputs[index]; + assert.equal(actual.replace(elapsedRegex, ""), expected.replace(elapsedRegex, ""), getOutputAtFailedMessage(caption, expected)); + index++; + } + + function assertWatchDiagnostic(diagnostic: Diagnostic) { + const expected = getWatchDiagnosticWithoutDate(diagnostic); + if (!disableConsoleClears && contains(screenStartingMessageCodes, diagnostic.code)) { + assert.equal(host.screenClears[screenClears], index, `Expected screen clear at this diagnostic: ${expected}`); + screenClears++; + } + assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected)); + index++; + } + + function getOutputAtFailedMessage(caption: string, expectedOutput: string) { + return `Expected ${caption}: ${JSON.stringify(expectedOutput)} at ${index} in ${JSON.stringify(outputs)}`; + } + + function getWatchDiagnosticWithoutDate(diagnostic: Diagnostic) { + const newLines = contains(screenStartingMessageCodes, diagnostic.code) + ? `${host.newLine}${host.newLine}` + : host.newLine; + return ` - ${flattenDiagnosticMessageText(diagnostic.messageText, host.newLine)}${newLines}`; + } + } + + function createErrorsFoundCompilerDiagnostic(errors: ReadonlyArray | ReadonlyArray) { + return errors.length === 1 + ? createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes) + : createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errors.length); + } + + export function checkOutputErrorsInitial(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, disableConsoleClears?: boolean, logsBeforeErrors?: string[]) { + checkOutputErrors( + host, + /*logsBeforeWatchDiagnostic*/ undefined, + createCompilerDiagnostic(Diagnostics.Starting_compilation_in_watch_mode), + logsBeforeErrors, + errors, + disableConsoleClears, + createErrorsFoundCompilerDiagnostic(errors)); + } + + export function checkOutputErrorsIncremental(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { + checkOutputErrors( + host, + logsBeforeWatchDiagnostic, + createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), + logsBeforeErrors, + errors, + disableConsoleClears, + createErrorsFoundCompilerDiagnostic(errors)); + } + + export function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, expectedExitCode: ExitStatus, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { + checkOutputErrors( + host, + logsBeforeWatchDiagnostic, + createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), + logsBeforeErrors, + errors, + disableConsoleClears); + assert.equal(host.exitCode, expectedExitCode); + } + + function getDiagnosticOfFileFrom(file: SourceFile | undefined, text: string, start: number | undefined, length: number | undefined, message: DiagnosticMessage): Diagnostic { + return { + file, + start, + length, + + messageText: text, + category: message.category, + code: message.code, + }; + } + + //function getDiagnosticWithoutFile(message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { + // let text = getLocaleSpecificMessage(message); + + // if (arguments.length > 1) { + // text = formatStringFromArgs(text, arguments, 1); + // } + + // return getDiagnosticOfFileFrom(/*file*/ undefined, text, /*start*/ undefined, /*length*/ undefined, message); + //} + + //function getDiagnosticOfFile(file: SourceFile, start: number, length: number, message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { + // let text = getLocaleSpecificMessage(message); + + // if (arguments.length > 4) { + // text = formatStringFromArgs(text, arguments, 4); + // } + + // return getDiagnosticOfFileFrom(file, text, start, length, message); + //} + + //function getUnknownCompilerOption(program: Program, configFile: File, option: string) { + // const quotedOption = `"${option}"`; + // return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); + //} + + export function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { + let text = getLocaleSpecificMessage(message); + + if (arguments.length > 5) { + text = formatStringFromArgs(text, arguments, 5); + } + + return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase()))!, + text, start, length, message); + } + + export function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) { + const quotedModuleName = `"${moduleName}"`; + return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); + } +} diff --git a/src/testRunner/unittests/tscWatchMode.ts b/src/testRunner/unittests/tscWatchMode.ts index 03bead8178b..662f26e8b13 100644 --- a/src/testRunner/unittests/tscWatchMode.ts +++ b/src/testRunner/unittests/tscWatchMode.ts @@ -1,45 +1,4 @@ namespace ts.tscWatch { - export import WatchedSystem = TestFSWithWatch.TestServerHost; - export type File = TestFSWithWatch.File; - export type SymLink = TestFSWithWatch.SymLink; - export import createWatchedSystem = TestFSWithWatch.createWatchedSystem; - export import checkArray = TestFSWithWatch.checkArray; - export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; - export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; - export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; - export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; - export import checkOutputContains = TestFSWithWatch.checkOutputContains; - export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; - - export function checkProgramActualFiles(program: Program, expectedFiles: ReadonlyArray) { - checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); - } - - export function checkProgramRootFiles(program: Program, expectedFiles: ReadonlyArray) { - checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); - } - - export function createWatchOfConfigFileReturningBuilder(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host); - compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; - const watch = createWatchProgram(compilerHost); - return () => watch.getCurrentProgram(); - } - - export function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host); - compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; - const watch = createWatchProgram(compilerHost); - return () => watch.getCurrentProgram().getProgram(); - } - - export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, host); - compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; - const watch = createWatchProgram(compilerHost); - return () => watch.getCurrentProgram().getProgram(); - } - function getEmittedLineForMultiFileOutput(file: File, host: WatchedSystem) { return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; } @@ -79,108 +38,6 @@ namespace ts.tscWatch { checkOutputDoesNotContain(host, expectedNonAffectedFiles); } - const elapsedRegex = /^Elapsed:: [0-9]+ms/; - function checkOutputErrors( - host: WatchedSystem, - logsBeforeWatchDiagnostic: string[] | undefined, - preErrorsWatchDiagnostic: Diagnostic, - logsBeforeErrors: string[] | undefined, - errors: ReadonlyArray | ReadonlyArray, - disableConsoleClears?: boolean | undefined, - ...postErrorsWatchDiagnostics: Diagnostic[] - ) { - let screenClears = 0; - const outputs = host.getOutput(); - const expectedOutputCount = 1 + errors.length + postErrorsWatchDiagnostics.length + - (logsBeforeWatchDiagnostic ? logsBeforeWatchDiagnostic.length : 0) + (logsBeforeErrors ? logsBeforeErrors.length : 0); - assert.equal(outputs.length, expectedOutputCount, JSON.stringify(outputs)); - let index = 0; - forEach(logsBeforeWatchDiagnostic, log => assertLog("logsBeforeWatchDiagnostic", log)); - assertWatchDiagnostic(preErrorsWatchDiagnostic); - forEach(logsBeforeErrors, log => assertLog("logBeforeError", log)); - // Verify errors - forEach(errors, assertDiagnostic); - forEach(postErrorsWatchDiagnostics, assertWatchDiagnostic); - assert.equal(host.screenClears.length, screenClears, "Expected number of screen clears"); - host.clearOutput(); - - function isDiagnostic(diagnostic: Diagnostic | string): diagnostic is Diagnostic { - return !!(diagnostic as Diagnostic).messageText; - } - - function assertDiagnostic(diagnostic: Diagnostic | string) { - const expected = isDiagnostic(diagnostic) ? formatDiagnostic(diagnostic, host) : diagnostic; - assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected)); - index++; - } - - function assertLog(caption: string, expected: string) { - const actual = outputs[index]; - assert.equal(actual.replace(elapsedRegex, ""), expected.replace(elapsedRegex, ""), getOutputAtFailedMessage(caption, expected)); - index++; - } - - function assertWatchDiagnostic(diagnostic: Diagnostic) { - const expected = getWatchDiagnosticWithoutDate(diagnostic); - if (!disableConsoleClears && contains(screenStartingMessageCodes, diagnostic.code)) { - assert.equal(host.screenClears[screenClears], index, `Expected screen clear at this diagnostic: ${expected}`); - screenClears++; - } - assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected)); - index++; - } - - function getOutputAtFailedMessage(caption: string, expectedOutput: string) { - return `Expected ${caption}: ${JSON.stringify(expectedOutput)} at ${index} in ${JSON.stringify(outputs)}`; - } - - function getWatchDiagnosticWithoutDate(diagnostic: Diagnostic) { - const newLines = contains(screenStartingMessageCodes, diagnostic.code) - ? `${host.newLine}${host.newLine}` - : host.newLine; - return ` - ${flattenDiagnosticMessageText(diagnostic.messageText, host.newLine)}${newLines}`; - } - } - - function createErrorsFoundCompilerDiagnostic(errors: ReadonlyArray | ReadonlyArray) { - return errors.length === 1 - ? createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes) - : createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errors.length); - } - - export function checkOutputErrorsInitial(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, disableConsoleClears?: boolean, logsBeforeErrors?: string[]) { - checkOutputErrors( - host, - /*logsBeforeWatchDiagnostic*/ undefined, - createCompilerDiagnostic(Diagnostics.Starting_compilation_in_watch_mode), - logsBeforeErrors, - errors, - disableConsoleClears, - createErrorsFoundCompilerDiagnostic(errors)); - } - - export function checkOutputErrorsIncremental(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { - checkOutputErrors( - host, - logsBeforeWatchDiagnostic, - createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), - logsBeforeErrors, - errors, - disableConsoleClears, - createErrorsFoundCompilerDiagnostic(errors)); - } - - function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, expectedExitCode: ExitStatus, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { - checkOutputErrors( - host, - logsBeforeWatchDiagnostic, - createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), - logsBeforeErrors, - errors, - disableConsoleClears); - assert.equal(host.exitCode, expectedExitCode); - } - function getDiagnosticOfFileFrom(file: SourceFile | undefined, text: string, start: number | undefined, length: number | undefined, message: DiagnosticMessage): Diagnostic { return { file, @@ -218,22 +75,6 @@ namespace ts.tscWatch { return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); } - function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { - let text = getLocaleSpecificMessage(message); - - if (arguments.length > 5) { - text = formatStringFromArgs(text, arguments, 5); - } - - return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase()))!, - text, start, length, message); - } - - function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) { - const quotedModuleName = `"${moduleName}"`; - return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); - } - describe("tsc-watch program updates", () => { const commonFile1: File = { path: "/a/b/commonFile1.ts", @@ -2343,411 +2184,6 @@ interface Document { }); }); - describe("tsc-watch module resolution caching", () => { - it("works", () => { - const root = { - path: "/a/d/f0.ts", - content: `import {x} from "f1"` - }; - const imported = { - path: "/a/f1.ts", - content: `foo()` - }; - - const files = [root, imported, libFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); - const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); - - // ensure that imported file was found - checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); - - const originalFileExists = host.fileExists; - { - const newContent = `import {x} from "f1" - var x: string = 1;`; - root.content = newContent; - host.reloadFS(files); - - // patch fileExists to make sure that disk is not touched - host.fileExists = notImplemented; - - // trigger synchronization to make sure that import will be fetched from the cache - host.runQueuedTimeoutCallbacks(); - - // ensure file has correct number of errors after edit - checkOutputErrorsIncremental(host, [ - f1IsNotModule, - getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), - cannotFindFoo - ]); - } - { - let fileExistsIsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsIsCalled = true; - assert.isTrue(fileName.indexOf("/f2.") !== -1); - return originalFileExists.call(host, fileName); - }; - - root.content = `import {x} from "f2"`; - host.reloadFS(files); - - // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk - host.runQueuedTimeoutCallbacks(); - - // ensure file has correct number of errors after edit - checkOutputErrorsIncremental(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") - ]); - - assert.isTrue(fileExistsIsCalled); - } - { - let fileExistsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsCalled = true; - assert.isTrue(fileName.indexOf("/f1.") !== -1); - return originalFileExists.call(host, fileName); - }; - - const newContent = `import {x} from "f1"`; - root.content = newContent; - - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - - checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); - assert.isTrue(fileExistsCalled); - } - }); - - it("loads missing files from disk", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; - - const imported = { - path: `/a/bar.d.ts`, - content: `export const y = 1;` - }; - - const files = [root, libFile]; - const host = createWatchedSystem(files); - const originalFileExists = host.fileExists; - - let fileExistsCalledForBar = false; - host.fileExists = fileName => { - if (fileName === "lib.d.ts") { - return false; - } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } - - return originalFileExists.call(host, fileName); - }; - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ]); - - fileExistsCalledForBar = false; - root.content = `import {y} from "bar"`; - host.reloadFS(files.concat(imported)); - - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - }); - - it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; - - const imported = { - path: `/a/bar.d.ts`, - content: `export const y = 1;export const x = 10;` - }; - - const files = [root, libFile]; - const filesWithImported = files.concat(imported); - const host = createWatchedSystem(filesWithImported); - const originalFileExists = host.fileExists; - let fileExistsCalledForBar = false; - host.fileExists = fileName => { - if (fileName === "lib.d.ts") { - return false; - } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } - return originalFileExists.call(host, fileName); - }; - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrorsInitial(host, emptyArray); - - fileExistsCalledForBar = false; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - checkOutputErrorsIncremental(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ]); - - fileExistsCalledForBar = false; - host.reloadFS(filesWithImported); - host.checkTimeoutQueueLengthAndRun(1); - checkOutputErrorsIncremental(host, emptyArray); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - }); - - it("works when module resolution changes to ambient module", () => { - const root = { - path: "/a/b/foo.ts", - content: `import * as fs from "fs";` - }; - - const packageJson = { - path: "/a/b/node_modules/@types/node/package.json", - content: ` -{ - "main": "" -} -` - }; - - const nodeType = { - path: "/a/b/node_modules/@types/node/index.d.ts", - content: ` -declare module "fs" { - export interface Stats { - isFile(): boolean; - } -}` - }; - - const files = [root, libFile]; - const filesWithNodeType = files.concat(packageJson, nodeType); - const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { }); - - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ]); - - host.reloadFS(filesWithNodeType); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("works when included file with ambient module changes", () => { - const root = { - path: "/a/b/foo.ts", - content: ` -import * as fs from "fs"; -import * as u from "url"; -` - }; - - const file = { - path: "/a/b/bar.d.ts", - content: ` -declare module "url" { - export interface Url { - href?: string; - } -} -` - }; - - const fileContentWithFS = ` -declare module "fs" { - export interface Stats { - isFile(): boolean; - } -} -`; - - const files = [root, file, libFile]; - const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); - - const watch = createWatchOfFilesAndCompilerOptions([root.path, file.path], host, {}); - - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ]); - - file.content += fileContentWithFS; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("works when reusing program with files from external library", () => { - interface ExpectedFile { path: string; isExpectedToEmit?: boolean; content?: string; } - const configDir = "/a/b/projects/myProject/src/"; - const file1: File = { - path: configDir + "file1.ts", - content: 'import module1 = require("module1");\nmodule1("hello");' - }; - const file2: File = { - path: configDir + "file2.ts", - content: 'import module11 = require("module1");\nmodule11("hello");' - }; - const module1: File = { - path: "/a/b/projects/myProject/node_modules/module1/index.js", - content: "module.exports = options => { return options.toString(); }" - }; - const configFile: File = { - path: configDir + "tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - allowJs: true, - rootDir: ".", - outDir: "../dist", - moduleResolution: "node", - maxNodeModuleJsDepth: 1 - } - }) - }; - const outDirFolder = "/a/b/projects/myProject/dist/"; - const programFiles = [file1, file2, module1, libFile]; - const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" }); - const watch = createWatchOfConfigFile(configFile.path, host); - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrorsInitial(host, emptyArray); - const expectedFiles: ExpectedFile[] = [ - createExpectedEmittedFile(file1), - createExpectedEmittedFile(file2), - createExpectedToNotEmitFile("index.js"), - createExpectedToNotEmitFile("src/index.js"), - createExpectedToNotEmitFile("src/file1.js"), - createExpectedToNotEmitFile("src/file2.js"), - createExpectedToNotEmitFile("lib.js"), - createExpectedToNotEmitFile("lib.d.ts") - ]; - verifyExpectedFiles(expectedFiles); - - file1.content += "\n;"; - expectedFiles[0].content += ";\n"; // Only emit file1 with this change - expectedFiles[1].isExpectedToEmit = false; - host.reloadFS(programFiles.concat(configFile)); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrorsIncremental(host, emptyArray); - verifyExpectedFiles(expectedFiles); - - - function verifyExpectedFiles(expectedFiles: ExpectedFile[]) { - forEach(expectedFiles, f => { - assert.equal(!!host.fileExists(f.path), f.isExpectedToEmit, "File " + f.path + " is expected to " + (f.isExpectedToEmit ? "emit" : "not emit")); - if (f.isExpectedToEmit) { - assert.equal(host.readFile(f.path), f.content, "Expected contents of " + f.path); - } - }); - } - - function createExpectedToNotEmitFile(fileName: string): ExpectedFile { - return { - path: outDirFolder + fileName, - isExpectedToEmit: false - }; - } - - function createExpectedEmittedFile(file: File): ExpectedFile { - return { - path: removeFileExtension(file.path.replace(configDir, outDirFolder)) + Extension.Js, - isExpectedToEmit: true, - content: '"use strict";\nexports.__esModule = true;\n' + file.content.replace("import", "var") + "\n" - }; - } - }); - - it("works when renaming node_modules folder that already contains @types folder", () => { - const currentDirectory = "/user/username/projects/myproject"; - const file: File = { - path: `${currentDirectory}/a.ts`, - content: `import * as q from "qqq";` - }; - const module: File = { - path: `${currentDirectory}/node_modules2/@types/qqq/index.d.ts`, - content: "export {}" - }; - const files = [file, module, libFile]; - const host = createWatchedSystem(files, { currentDirectory }); - const watch = createWatchOfFilesAndCompilerOptions([file.path], host); - - checkProgramActualFiles(watch(), [file.path, libFile.path]); - checkOutputErrorsInitial(host, [getDiagnosticModuleNotFoundOfFile(watch(), file, "qqq")]); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectories(host, [`${currentDirectory}/node_modules`, `${currentDirectory}/node_modules/@types`], /*recursive*/ true); - - host.renameFolder(`${currentDirectory}/node_modules2`, `${currentDirectory}/node_modules`); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), [file.path, libFile.path, `${currentDirectory}/node_modules/@types/qqq/index.d.ts`]); - checkOutputErrorsIncremental(host, emptyArray); - }); - - describe("ignores files/folder changes in node_modules that start with '.'", () => { - const projectPath = "/user/username/projects/project"; - const npmCacheFile: File = { - path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, - content: JSON.stringify({ something: 10 }) - }; - const file1: File = { - path: `${projectPath}/test.ts`, - content: `import { x } from "somemodule";` - }; - const file2: File = { - path: `${projectPath}/node_modules/somemodule/index.d.ts`, - content: `export const x = 10;` - }; - const files = [libFile, file1, file2]; - const expectedFiles = files.map(f => f.path); - it("when watching node_modules in inferred project for failed lookup", () => { - const host = createWatchedSystem(files); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host, {}, /*maxNumberOfFilesToIterateForInvalidation*/ 1); - checkProgramActualFiles(watch(), expectedFiles); - host.checkTimeoutQueueLength(0); - - host.ensureFileOrFolder(npmCacheFile); - host.checkTimeoutQueueLength(0); - }); - it("when watching node_modules as part of wild card directories in config project", () => { - const config: File = { - path: `${projectPath}/tsconfig.json`, - content: "{}" - }; - const host = createWatchedSystem(files.concat(config)); - const watch = createWatchOfConfigFile(config.path, host); - checkProgramActualFiles(watch(), expectedFiles); - host.checkTimeoutQueueLength(0); - - host.ensureFileOrFolder(npmCacheFile); - host.checkTimeoutQueueLength(0); - }); - }); - }); - describe("tsc-watch with when module emit is specified as node", () => { it("when instead of filechanged recursive directory watcher is invoked", () => { const configFile: File = { @@ -2883,85 +2319,4 @@ declare module "fs" { }); }); }); - - describe("tsc-watch with modules linked to sibling folder", () => { - const projectRoot = "/user/username/projects/project"; - const mainPackageRoot = `${projectRoot}/main`; - const linkedPackageRoot = `${projectRoot}/linked-package`; - const mainFile: File = { - path: `${mainPackageRoot}/index.ts`, - content: "import { Foo } from '@scoped/linked-package'" - }; - const config: File = { - path: `${mainPackageRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, - files: ["index.ts"] - }) - }; - const linkedPackageInMain: SymLink = { - path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, - symLink: `${linkedPackageRoot}` - }; - const linkedPackageJson: File = { - path: `${linkedPackageRoot}/package.json`, - content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) - }; - const linkedPackageIndex: File = { - path: `${linkedPackageRoot}/dist/index.d.ts`, - content: "export * from './other';" - }; - const linkedPackageOther: File = { - path: `${linkedPackageRoot}/dist/other.d.ts`, - content: 'export declare const Foo = "BAR";' - }; - - it("verify watched directories", () => { - const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; - const host = createWatchedSystem(files, { currentDirectory: mainPackageRoot }); - createWatchOfConfigFile("tsconfig.json", host); - checkWatchedFilesDetailed(host, [libFile.path, mainFile.path, config.path, linkedPackageIndex.path, linkedPackageOther.path], 1); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectoriesDetailed(host, [`${mainPackageRoot}/@scoped`, `${mainPackageRoot}/node_modules`, linkedPackageRoot, `${mainPackageRoot}/node_modules/@types`, `${projectRoot}/node_modules/@types`], 1, /*recursive*/ true); - }); - }); - - describe("tsc-watch with custom module resolution", () => { - const projectRoot = "/user/username/projects/project"; - const configFileJson: any = { - compilerOptions: { module: "commonjs", resolveJsonModule: true }, - files: ["index.ts"] - }; - const mainFile: File = { - path: `${projectRoot}/index.ts`, - content: "import settings from './settings.json';" - }; - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify(configFileJson) - }; - const settingsJson: File = { - path: `${projectRoot}/settings.json`, - content: JSON.stringify({ content: "Print this" }) - }; - - it("verify that module resolution with json extension works when returned without extension", () => { - const files = [libFile, mainFile, config, settingsJson]; - const host = createWatchedSystem(files, { currentDirectory: projectRoot }); - const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, host); - const parsedCommandResult = parseJsonConfigFileContent(configFileJson, host, config.path); - compilerHost.resolveModuleNames = (moduleNames, containingFile) => moduleNames.map(m => { - const result = resolveModuleName(m, containingFile, parsedCommandResult.options, compilerHost); - const resolvedModule = result.resolvedModule!; - return { - resolvedFileName: resolvedModule.resolvedFileName, - isExternalLibraryImport: resolvedModule.isExternalLibraryImport, - originalFileName: resolvedModule.originalPath, - }; - }); - const watch = createWatchProgram(compilerHost); - const program = watch.getCurrentProgram().getProgram(); - checkProgramActualFiles(program, [mainFile.path, libFile.path, settingsJson.path]); - }); - }); } diff --git a/src/testRunner/unittests/tsserverHelpers.ts b/src/testRunner/unittests/tsserverHelpers.ts new file mode 100644 index 00000000000..5162f17d6ae --- /dev/null +++ b/src/testRunner/unittests/tsserverHelpers.ts @@ -0,0 +1,679 @@ +namespace ts.projectSystem { + export import TI = server.typingsInstaller; + export import protocol = server.protocol; + export import CommandNames = server.CommandNames; + + export import TestServerHost = TestFSWithWatch.TestServerHost; + export type File = TestFSWithWatch.File; + export type SymLink = TestFSWithWatch.SymLink; + export type Folder = TestFSWithWatch.Folder; + export import createServerHost = TestFSWithWatch.createServerHost; + export import checkArray = TestFSWithWatch.checkArray; + export import libFile = TestFSWithWatch.libFile; + export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; + export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; + export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; + export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; + + //const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; + //function mapOutputToJson(s: string) { + // return convertToObject( + // parseJsonText("json.json", s.replace(outputEventRegex, "")), + // [] + // ); + //} + + export const customTypesMap = { + path: "/typesMap.json", + content: `{ + "typesMap": { + "jquery": { + "match": "jquery(-(\\\\.?\\\\d+)+)?(\\\\.intellisense)?(\\\\.min)?\\\\.js$", + "types": ["jquery"] + }, + "quack": { + "match": "/duckquack-(\\\\d+)\\\\.min\\\\.js", + "types": ["duck-types"] + } + }, + "simpleMap": { + "Bacon": "baconjs", + "bliss": "blissfuljs", + "commander": "commander", + "cordova": "cordova", + "react": "react", + "lodash": "lodash" + } + }` + }; + + export interface PostExecAction { + readonly success: boolean; + readonly callback: TI.RequestCompletedAction; + } + + export const nullLogger: server.Logger = { + close: noop, + hasLevel: () => false, + loggingEnabled: () => false, + perftrc: noop, + info: noop, + msg: noop, + startGroup: noop, + endGroup: noop, + getLogFileName: () => undefined, + }; + + export function createHasErrorMessageLogger() { + let hasErrorMsg = false; + const { close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc } = nullLogger; + const logger: server.Logger = { + close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc, + msg: (s, type) => { + Debug.fail(`Error: ${s}, type: ${type}`); + hasErrorMsg = true; + } + }; + return { logger, hasErrorMsg: () => hasErrorMsg }; + } + + export class TestTypingsInstaller extends TI.TypingsInstaller implements server.ITypingsInstaller { + protected projectService!: server.ProjectService; + constructor( + readonly globalTypingsCacheLocation: string, + throttleLimit: number, + installTypingHost: server.ServerHost, + readonly typesRegistry = createMap>(), + log?: TI.Log) { + super(installTypingHost, globalTypingsCacheLocation, TestFSWithWatch.safeList.path, customTypesMap.path, throttleLimit, log); + } + + protected postExecActions: PostExecAction[] = []; + + isKnownTypesPackageName = notImplemented; + installPackage = notImplemented; + inspectValue = notImplemented; + + executePendingCommands() { + const actionsToRun = this.postExecActions; + this.postExecActions = []; + for (const action of actionsToRun) { + action.callback(action.success); + } + } + + checkPendingCommands(expectedCount: number) { + assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`); + } + + onProjectClosed = noop; + + attach(projectService: server.ProjectService) { + this.projectService = projectService; + } + + getInstallTypingHost() { + return this.installTypingHost; + } + + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { + this.addPostExecAction("success", cb); + } + + sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) { + this.projectService.updateTypingsForProject(response); + } + + enqueueInstallTypingsRequest(project: server.Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray) { + const request = server.createInstallTypingsRequest(project, typeAcquisition, unresolvedImports, this.globalTypingsCacheLocation); + this.install(request); + } + + addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) { + const out = isString(stdout) ? stdout : createNpmPackageJsonString(stdout); + const action: PostExecAction = { + success: !!out, + callback: cb + }; + this.postExecActions.push(action); + } + } + + function createNpmPackageJsonString(installedTypings: string[]): string { + const dependencies: MapLike = {}; + for (const typing of installedTypings) { + dependencies[typing] = "1.0.0"; + } + return JSON.stringify({ dependencies }); + } + + export function createTypesRegistry(...list: string[]): Map> { + const versionMap = { + "latest": "1.3.0", + "ts2.0": "1.0.0", + "ts2.1": "1.0.0", + "ts2.2": "1.2.0", + "ts2.3": "1.3.0", + "ts2.4": "1.3.0", + "ts2.5": "1.3.0", + "ts2.6": "1.3.0", + "ts2.7": "1.3.0" + }; + const map = createMap>(); + for (const l of list) { + map.set(l, versionMap); + } + return map; + } + + export function toExternalFile(fileName: string): protocol.ExternalFile { + return { fileName }; + } + + export function toExternalFiles(fileNames: string[]) { + return map(fileNames, toExternalFile); + } + + export function fileStats(nonZeroStats: Partial): server.FileStats { + return { ts: 0, tsSize: 0, tsx: 0, tsxSize: 0, dts: 0, dtsSize: 0, js: 0, jsSize: 0, jsx: 0, jsxSize: 0, deferred: 0, deferredSize: 0, ...nonZeroStats }; + } + + export interface ConfigFileDiagnostic { + fileName: string | undefined; + start: number | undefined; + length: number | undefined; + messageText: string; + category: DiagnosticCategory; + code: number; + reportsUnnecessary?: {}; + source?: string; + relatedInformation?: DiagnosticRelatedInformation[]; + } + + export class TestServerEventManager { + private events: server.ProjectServiceEvent[] = []; + readonly session: TestSession; + readonly service: server.ProjectService; + readonly host: TestServerHost; + constructor(files: File[], suppressDiagnosticEvents?: boolean) { + this.host = createServerHost(files); + this.session = createSession(this.host, { + canUseEvents: true, + eventHandler: event => this.events.push(event), + suppressDiagnosticEvents, + }); + this.service = this.session.getProjectService(); + } + + getEvents(): ReadonlyArray { + const events = this.events; + this.events = []; + return events; + } + + getEvent(eventName: T["eventName"]): T["data"] { + let eventData: T["data"] | undefined; + filterMutate(this.events, e => { + if (e.eventName === eventName) { + if (eventData !== undefined) { + assert(false, "more than one event found"); + } + eventData = e.data; + return false; + } + return true; + }); + return Debug.assertDefined(eventData); + } + + hasZeroEvent(eventName: T["eventName"]) { + this.events.forEach(event => assert.notEqual(event.eventName, eventName)); + } + + checkSingleConfigFileDiagEvent(configFileName: string, triggerFile: string, errors: ReadonlyArray) { + const eventData = this.getEvent(server.ConfigFileDiagEvent); + assert.equal(eventData.configFileName, configFileName); + assert.equal(eventData.triggerFile, triggerFile); + const actual = eventData.diagnostics.map(({ file, messageText, ...rest }) => ({ fileName: file && file.fileName, messageText: isString(messageText) ? messageText : "", ...rest })); + if (errors) { + assert.deepEqual(actual, errors); + } + } + + assertProjectInfoTelemetryEvent(partial: Partial, configFile = "/tsconfig.json"): void { + assert.deepEqual(this.getEvent(server.ProjectInfoTelemetryEvent), { + projectId: sys.createSHA256Hash!(configFile), + fileStats: fileStats({ ts: 1 }), + compilerOptions: {}, + extends: false, + files: false, + include: false, + exclude: false, + compileOnSave: false, + typeAcquisition: { + enable: false, + exclude: false, + include: false, + }, + configFileName: "tsconfig.json", + projectType: "configured", + languageServiceEnabled: true, + version, + ...partial, + }); + } + + assertOpenFileTelemetryEvent(info: server.OpenFileInfo): void { + assert.deepEqual(this.getEvent(server.OpenFileInfoTelemetryEvent), { info }); + } + assertNoOpenFilesTelemetryEvent(): void { + this.hasZeroEvent(server.OpenFileInfoTelemetryEvent); + } + } + + export class TestSession extends server.Session { + private seq = 0; + public events: protocol.Event[] = []; + public host!: TestServerHost; + + getProjectService() { + return this.projectService; + } + + public getSeq() { + return this.seq; + } + + public getNextSeq() { + return this.seq + 1; + } + + public executeCommandSeq(request: Partial) { + this.seq++; + request.seq = this.seq; + request.type = "request"; + return this.executeCommand(request); + } + + public event(body: T, eventName: string) { + this.events.push(server.toEvent(eventName, body)); + super.event(body, eventName); + } + + public clearMessages() { + clear(this.events); + this.host.clearOutput(); + } + } + + export function createSession(host: server.ServerHost, opts: Partial = {}) { + if (opts.typingsInstaller === undefined) { + opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); + } + + if (opts.eventHandler !== undefined) { + opts.canUseEvents = true; + } + + const sessionOptions: server.SessionOptions = { + host, + cancellationToken: server.nullCancellationToken, + useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, + typingsInstaller: undefined!, // TODO: GH#18217 + byteLength: Utils.byteLength, + hrtime: process.hrtime, + logger: opts.logger || createHasErrorMessageLogger().logger, + canUseEvents: false + }; + + return new TestSession({ ...sessionOptions, ...opts }); + } + + //function createSessionWithEventTracking(host: server.ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) { + // const events: T[] = []; + // const session = createSession(host, { + // eventHandler: e => { + // if (e.eventName === eventName || eventNames.some(eventName => e.eventName === eventName)) { + // events.push(e as T); + // } + // } + // }); + + // return { session, events }; + //} + + //function createSessionWithDefaultEventHandler(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial = {}) { + // const session = createSession(host, { canUseEvents: true, ...opts }); + + // return { + // session, + // getEvents, + // clearEvents + // }; + + // function getEvents() { + // return mapDefined(host.getOutput(), s => { + // const e = mapOutputToJson(s); + // return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined; + // }); + // } + + // function clearEvents() { + // session.clearMessages(); + // } + //} + + export interface CreateProjectServiceParameters { + cancellationToken?: HostCancellationToken; + logger?: server.Logger; + useSingleInferredProject?: boolean; + typingsInstaller?: server.ITypingsInstaller; + eventHandler?: server.ProjectServiceEventHandler; + } + + export class TestProjectService extends server.ProjectService { + constructor(host: server.ServerHost, logger: server.Logger, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, + typingsInstaller: server.ITypingsInstaller, eventHandler: server.ProjectServiceEventHandler, opts: Partial = {}) { + super({ + host, + logger, + cancellationToken, + useSingleInferredProject, + useInferredProjectPerProjectRoot: false, + typingsInstaller, + typesMapLocation: customTypesMap.path, + eventHandler, + ...opts + }); + } + + checkNumberOfProjects(count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { + checkNumberOfProjects(this, count); + } + } + export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}, options?: Partial) { + const cancellationToken = parameters.cancellationToken || server.nullCancellationToken; + const logger = parameters.logger || createHasErrorMessageLogger().logger; + const useSingleInferredProject = parameters.useSingleInferredProject !== undefined ? parameters.useSingleInferredProject : false; + return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller!, parameters.eventHandler!, options); // TODO: GH#18217 + } + + export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); + } + + export function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`); + } + + export function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); + } + + export function checkNumberOfProjects(projectService: server.ProjectService, count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { + checkNumberOfConfiguredProjects(projectService, count.configuredProjects || 0); + checkNumberOfExternalProjects(projectService, count.externalProjects || 0); + checkNumberOfInferredProjects(projectService, count.inferredProjects || 0); + } + + export function configuredProjectAt(projectService: server.ProjectService, index: number) { + const values = projectService.configuredProjects.values(); + while (index > 0) { + values.next(); + index--; + } + return values.next().value; + } + + export function checkProjectActualFiles(project: server.Project, expectedFiles: ReadonlyArray) { + checkArray(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); + } + + //function checkProjectRootFiles(project: server.Project, expectedFiles: ReadonlyArray) { + // checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); + //} + + export function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { + dir = normalizePath(dir); + const result: string[] = []; + forEachAncestorDirectory(dir, ancestor => { + if (mapAncestor(ancestor)) { + result.push(combinePaths(ancestor, path2)); + } + }); + return result; + } + + export function getRootsToWatchWithAncestorDirectory(dir: string, path2: string) { + return mapCombinedPathsInAncestor(dir, path2, ancestor => ancestor.split(directorySeparator).length > 4); + } + + export 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); + //} + + //function checkOpenFiles(projectService: server.ProjectService, expectedFiles: File[]) { + // checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path)); + //} + + //function protocolLocationFromSubstring(str: string, substring: string): protocol.Location { + // const start = str.indexOf(substring); + // Debug.assert(start !== -1); + // return protocolToLocation(str)(start); + //} + //function protocolToLocation(text: string): (pos: number) => protocol.Location { + // const lineStarts = computeLineStarts(text); + // return pos => { + // const x = computeLineAndCharacterOfPosition(lineStarts, pos); + // return { line: x.line + 1, offset: x.character + 1 }; + // }; + //} + //function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan { + // const span = textSpanFromSubstring(str, substring, options); + // const toLocation = protocolToLocation(str); + // return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) }; + //} + //function protocolRenameSpanFromSubstring( + // str: string, + // substring: string, + // options?: SpanFromSubstringOptions, + // prefixSuffixText?: { readonly prefixText?: string, readonly suffixText?: string }, + //): protocol.RenameTextSpan { + // return { ...protocolTextSpanFromSubstring(str, substring, options), ...prefixSuffixText }; + //} + //function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan { + // const start = nthIndexOf(str, substring, options ? options.index : 0); + // Debug.assert(start !== -1); + // return createTextSpan(start, substring.length); + //} + //function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs { + // return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) }; + //} + //function protocolFileSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileSpan { + // return { file: file.path, ...protocolTextSpanFromSubstring(file.content, substring, options) }; + //} + //function documentSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): DocumentSpan { + // return { fileName: file.path, textSpan: textSpanFromSubstring(file.content, substring, options) }; + //} + //function renameLocation(file: File, substring: string, options?: SpanFromSubstringOptions): RenameLocation { + // return documentSpanFromSubstring(file, substring, options); + //} + //interface SpanFromSubstringOptions { + // readonly index: number; + //} + + //function nthIndexOf(str: string, substr: string, n: number): number { + // let index = -1; + // for (; n >= 0; n--) { + // index = str.indexOf(substr, index + 1); + // if (index === -1) return -1; + // } + // return index; + //} + + /** + * Test server cancellation token used to mock host token cancellation requests. + * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls + * should be made before canceling the token. The id of the request to cancel should be set with + * setRequestToCancel(); + */ + export class TestServerCancellationToken implements server.ServerCancellationToken { + private currentId: number | undefined = -1; + private requestToCancel = -1; + private isCancellationRequestedCount = 0; + + constructor(private cancelAfterRequest = 0) { + } + + setRequest(requestId: number) { + this.currentId = requestId; + } + + setRequestToCancel(requestId: number) { + this.resetToken(); + this.requestToCancel = requestId; + } + + resetRequest(requestId: number) { + assert.equal(requestId, this.currentId, "unexpected request id in cancellation"); + this.currentId = undefined; + } + + isCancellationRequested() { + this.isCancellationRequestedCount++; + // If the request id is the request to cancel and isCancellationRequestedCount + // has been met then cancel the request. Ex: cancel the request if it is a + // nav bar request & isCancellationRequested() has already been called three times. + return this.requestToCancel === this.currentId && this.isCancellationRequestedCount >= this.cancelAfterRequest; + } + + resetToken() { + this.currentId = -1; + this.isCancellationRequestedCount = 0; + this.requestToCancel = -1; + } + } + + export function makeSessionRequest(command: string, args: T): protocol.Request { + return { + seq: 0, + type: "request", + command, + arguments: args + }; + } + + export function executeSessionRequest(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] { + return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"]; + } + + export function executeSessionRequestNoResponse(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): void { + session.executeCommand(makeSessionRequest(command, args)); + } + + export function openFilesForSession(files: ReadonlyArray, session: server.Session): void { + for (const file of files) { + session.executeCommand(makeSessionRequest(CommandNames.Open, + "projectRootPath" in file ? { file: typeof file.file === "string" ? file.file : file.file.path, projectRootPath: file.projectRootPath } : { file: file.path })); + } + } + + export function closeFilesForSession(files: ReadonlyArray, session: server.Session): void { + for (const file of files) { + session.executeCommand(makeSessionRequest(CommandNames.Close, { file: file.path })); + } + } + + //interface ErrorInformation { + // diagnosticMessage: DiagnosticMessage; + // errorTextArguments?: string[]; + //} + + //function getProtocolDiagnosticMessage({ diagnosticMessage, errorTextArguments = [] }: ErrorInformation) { + // return formatStringFromArgs(diagnosticMessage.message, errorTextArguments); + //} + + //function verifyDiagnostics(actual: server.protocol.Diagnostic[], expected: ErrorInformation[]) { + // const expectedErrors = expected.map(getProtocolDiagnosticMessage); + // assert.deepEqual(actual.map(diag => flattenDiagnosticMessageText(diag.text, "\n")), expectedErrors); + //} + + //function verifyNoDiagnostics(actual: server.protocol.Diagnostic[]) { + // verifyDiagnostics(actual, []); + //} + + //function checkErrorMessage(session: TestSession, eventName: protocol.DiagnosticEventKind, diagnostics: protocol.DiagnosticEventBody, isMostRecent = false): void { + // checkNthEvent(session, server.toEvent(eventName, diagnostics), 0, isMostRecent); + //} + + //function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: ReadonlyArray = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}, relatedInformation?: protocol.DiagnosticRelatedInformation[]): protocol.Diagnostic { + // return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, relatedInformation, source: undefined }; + //} + + //function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number, isMostRecent = true): void { + // checkNthEvent(session, server.toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, isMostRecent); + //} + + //function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) { + // checkNthEvent(session, server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); + //} + + //function checkNoDiagnosticEvents(session: TestSession) { + // for (const event of session.events) { + // assert.isFalse(event.event.endsWith("Diag"), JSON.stringify(event)); + // } + //} + + //function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { + // const events = session.events; + // assert.deepEqual(events[index], expectedEvent, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`); + + // const outputs = session.host.getOutput(); + // assert.equal(outputs[index], server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, session.host.newLine)); + + // if (isMostRecent) { + // assert.strictEqual(events.length, index + 1, JSON.stringify(events)); + // assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs)); + // } + //} + + //function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem { + // return { + // ...protocolFileSpanFromSubstring(file, text, options), + // isDefinition, + // isWriteAccess: isDefinition, + // lineText, + // }; + //} + + //function makeReferenceEntry(file: File, isDefinition: boolean, text: string, options?: SpanFromSubstringOptions): ReferenceEntry { + // return { + // ...documentSpanFromSubstring(file, text, options), + // isDefinition, + // isWriteAccess: isDefinition, + // isInString: undefined, + // }; + //} + + //function checkDeclarationFiles(file: File, session: TestSession, expectedFiles: ReadonlyArray): void { + // openFilesForSession([file], session); + // const project = Debug.assertDefined(session.getProjectService().getDefaultProjectForFile(file.path as server.NormalizedPath, /*ensureProject*/ false)); + // const program = project.getCurrentProgram()!; + // const output = getFileEmitOutput(program, Debug.assertDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true); + // closeFilesForSession([file], session); + + // Debug.assert(!output.emitSkipped); + // assert.deepEqual(output.outputFiles, expectedFiles.map((e): OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false }))); + //} +} diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index cefa8a5e34c..3b7c276cfc4 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -1,21 +1,4 @@ namespace ts.projectSystem { - import TI = server.typingsInstaller; - import protocol = server.protocol; - import CommandNames = server.CommandNames; - - export import TestServerHost = TestFSWithWatch.TestServerHost; - export type File = TestFSWithWatch.File; - export type SymLink = TestFSWithWatch.SymLink; - export type Folder = TestFSWithWatch.Folder; - export import createServerHost = TestFSWithWatch.createServerHost; - export import checkArray = TestFSWithWatch.checkArray; - export import libFile = TestFSWithWatch.libFile; - export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; - export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; - export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; - export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; - import safeList = TestFSWithWatch.safeList; - const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; function mapOutputToJson(s: string) { return convertToObject( @@ -24,319 +7,6 @@ namespace ts.projectSystem { ); } - export const customTypesMap = { - path: "/typesMap.json", - content: `{ - "typesMap": { - "jquery": { - "match": "jquery(-(\\\\.?\\\\d+)+)?(\\\\.intellisense)?(\\\\.min)?\\\\.js$", - "types": ["jquery"] - }, - "quack": { - "match": "/duckquack-(\\\\d+)\\\\.min\\\\.js", - "types": ["duck-types"] - } - }, - "simpleMap": { - "Bacon": "baconjs", - "bliss": "blissfuljs", - "commander": "commander", - "cordova": "cordova", - "react": "react", - "lodash": "lodash" - } - }` - }; - - export interface PostExecAction { - readonly success: boolean; - readonly callback: TI.RequestCompletedAction; - } - - export const nullLogger: server.Logger = { - close: noop, - hasLevel: () => false, - loggingEnabled: () => false, - perftrc: noop, - info: noop, - msg: noop, - startGroup: noop, - endGroup: noop, - getLogFileName: () => undefined, - }; - - export function createHasErrorMessageLogger() { - let hasErrorMsg = false; - const { close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc } = nullLogger; - const logger: server.Logger = { - close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc, - msg: (s, type) => { - Debug.fail(`Error: ${s}, type: ${type}`); - hasErrorMsg = true; - } - }; - return { logger, hasErrorMsg: () => hasErrorMsg }; - } - - export class TestTypingsInstaller extends TI.TypingsInstaller implements server.ITypingsInstaller { - protected projectService!: server.ProjectService; - constructor( - readonly globalTypingsCacheLocation: string, - throttleLimit: number, - installTypingHost: server.ServerHost, - readonly typesRegistry = createMap>(), - log?: TI.Log) { - super(installTypingHost, globalTypingsCacheLocation, safeList.path, customTypesMap.path, throttleLimit, log); - } - - protected postExecActions: PostExecAction[] = []; - - isKnownTypesPackageName = notImplemented; - installPackage = notImplemented; - inspectValue = notImplemented; - - executePendingCommands() { - const actionsToRun = this.postExecActions; - this.postExecActions = []; - for (const action of actionsToRun) { - action.callback(action.success); - } - } - - checkPendingCommands(expectedCount: number) { - assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`); - } - - onProjectClosed = noop; - - attach(projectService: server.ProjectService) { - this.projectService = projectService; - } - - getInstallTypingHost() { - return this.installTypingHost; - } - - installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { - this.addPostExecAction("success", cb); - } - - sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) { - this.projectService.updateTypingsForProject(response); - } - - enqueueInstallTypingsRequest(project: server.Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray) { - const request = server.createInstallTypingsRequest(project, typeAcquisition, unresolvedImports, this.globalTypingsCacheLocation); - this.install(request); - } - - addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) { - const out = isString(stdout) ? stdout : createNpmPackageJsonString(stdout); - const action: PostExecAction = { - success: !!out, - callback: cb - }; - this.postExecActions.push(action); - } - } - - function createNpmPackageJsonString(installedTypings: string[]): string { - const dependencies: MapLike = {}; - for (const typing of installedTypings) { - dependencies[typing] = "1.0.0"; - } - return JSON.stringify({ dependencies }); - } - - export function createTypesRegistry(...list: string[]): Map> { - const versionMap = { - "latest": "1.3.0", - "ts2.0": "1.0.0", - "ts2.1": "1.0.0", - "ts2.2": "1.2.0", - "ts2.3": "1.3.0", - "ts2.4": "1.3.0", - "ts2.5": "1.3.0", - "ts2.6": "1.3.0", - "ts2.7": "1.3.0" - }; - const map = createMap>(); - for (const l of list) { - map.set(l, versionMap); - } - 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 }; - } - - export function toExternalFiles(fileNames: string[]) { - return map(fileNames, toExternalFile); - } - - export function fileStats(nonZeroStats: Partial): server.FileStats { - return { ts: 0, tsSize: 0, tsx: 0, tsxSize: 0, dts: 0, dtsSize: 0, js: 0, jsSize: 0, jsx: 0, jsxSize: 0, deferred: 0, deferredSize: 0, ...nonZeroStats }; - } - - export interface ConfigFileDiagnostic { - fileName: string | undefined; - start: number | undefined; - length: number | undefined; - messageText: string; - category: DiagnosticCategory; - code: number; - reportsUnnecessary?: {}; - source?: string; - relatedInformation?: DiagnosticRelatedInformation[]; - } - - export class TestServerEventManager { - private events: server.ProjectServiceEvent[] = []; - readonly session: TestSession; - readonly service: server.ProjectService; - readonly host: TestServerHost; - constructor(files: File[], suppressDiagnosticEvents?: boolean) { - this.host = createServerHost(files); - this.session = createSession(this.host, { - canUseEvents: true, - eventHandler: event => this.events.push(event), - suppressDiagnosticEvents, - }); - this.service = this.session.getProjectService(); - } - - getEvents(): ReadonlyArray { - const events = this.events; - this.events = []; - return events; - } - - getEvent(eventName: T["eventName"]): T["data"] { - let eventData: T["data"] | undefined; - filterMutate(this.events, e => { - if (e.eventName === eventName) { - if (eventData !== undefined) { - assert(false, "more than one event found"); - } - eventData = e.data; - return false; - } - return true; - }); - return Debug.assertDefined(eventData); - } - - hasZeroEvent(eventName: T["eventName"]) { - this.events.forEach(event => assert.notEqual(event.eventName, eventName)); - } - - checkSingleConfigFileDiagEvent(configFileName: string, triggerFile: string, errors: ReadonlyArray) { - const eventData = this.getEvent(server.ConfigFileDiagEvent); - assert.equal(eventData.configFileName, configFileName); - assert.equal(eventData.triggerFile, triggerFile); - const actual = eventData.diagnostics.map(({ file, messageText, ...rest }) => ({ fileName: file && file.fileName, messageText: isString(messageText) ? messageText : "", ...rest })); - if (errors) { - assert.deepEqual(actual, errors); - } - } - - assertProjectInfoTelemetryEvent(partial: Partial, configFile = "/tsconfig.json"): void { - assert.deepEqual(this.getEvent(server.ProjectInfoTelemetryEvent), { - projectId: sys.createSHA256Hash!(configFile), - fileStats: fileStats({ ts: 1 }), - compilerOptions: {}, - extends: false, - files: false, - include: false, - exclude: false, - compileOnSave: false, - typeAcquisition: { - enable: false, - exclude: false, - include: false, - }, - configFileName: "tsconfig.json", - projectType: "configured", - languageServiceEnabled: true, - version, - ...partial, - }); - } - - assertOpenFileTelemetryEvent(info: server.OpenFileInfo): void { - assert.deepEqual(this.getEvent(server.OpenFileInfoTelemetryEvent), { info }); - } - assertNoOpenFilesTelemetryEvent(): void { - this.hasZeroEvent(server.OpenFileInfoTelemetryEvent); - } - } - - class TestSession extends server.Session { - private seq = 0; - public events: protocol.Event[] = []; - public host!: TestServerHost; - - getProjectService() { - return this.projectService; - } - - public getSeq() { - return this.seq; - } - - public getNextSeq() { - return this.seq + 1; - } - - public executeCommandSeq(request: Partial) { - this.seq++; - request.seq = this.seq; - request.type = "request"; - return this.executeCommand(request); - } - - public event(body: T, eventName: string) { - this.events.push(server.toEvent(eventName, body)); - super.event(body, eventName); - } - - public clearMessages() { - clear(this.events); - this.host.clearOutput(); - } - } - - export function createSession(host: server.ServerHost, opts: Partial = {}) { - if (opts.typingsInstaller === undefined) { - opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); - } - - if (opts.eventHandler !== undefined) { - opts.canUseEvents = true; - } - - const sessionOptions: server.SessionOptions = { - host, - cancellationToken: server.nullCancellationToken, - useSingleInferredProject: false, - useInferredProjectPerProjectRoot: false, - typingsInstaller: undefined!, // TODO: GH#18217 - byteLength: Utils.byteLength, - hrtime: process.hrtime, - logger: opts.logger || createHasErrorMessageLogger().logger, - canUseEvents: false - }; - - return new TestSession({ ...sessionOptions, ...opts }); - } - function createSessionWithEventTracking(host: server.ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) { const events: T[] = []; const session = createSession(host, { @@ -371,101 +41,14 @@ namespace ts.projectSystem { } } - interface CreateProjectServiceParameters { - cancellationToken?: HostCancellationToken; - logger?: server.Logger; - useSingleInferredProject?: boolean; - typingsInstaller?: server.ITypingsInstaller; - eventHandler?: server.ProjectServiceEventHandler; - } - - export class TestProjectService extends server.ProjectService { - constructor(host: server.ServerHost, logger: server.Logger, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, - typingsInstaller: server.ITypingsInstaller, eventHandler: server.ProjectServiceEventHandler, opts: Partial = {}) { - super({ - host, - logger, - cancellationToken, - useSingleInferredProject, - useInferredProjectPerProjectRoot: false, - typingsInstaller, - typesMapLocation: customTypesMap.path, - eventHandler, - ...opts - }); - } - - checkNumberOfProjects(count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { - checkNumberOfProjects(this, count); - } - } - export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}, options?: Partial) { - const cancellationToken = parameters.cancellationToken || server.nullCancellationToken; - const logger = parameters.logger || createHasErrorMessageLogger().logger; - const useSingleInferredProject = parameters.useSingleInferredProject !== undefined ? parameters.useSingleInferredProject : false; - return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller!, parameters.eventHandler!, options); // TODO: GH#18217 - } - - export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { - assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); - } - - function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) { - assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`); - } - - function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) { - assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); - } - - export function checkNumberOfProjects(projectService: server.ProjectService, count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { - checkNumberOfConfiguredProjects(projectService, count.configuredProjects || 0); - checkNumberOfExternalProjects(projectService, count.externalProjects || 0); - checkNumberOfInferredProjects(projectService, count.inferredProjects || 0); - } - - export function configuredProjectAt(projectService: server.ProjectService, index: number) { - const values = projectService.configuredProjects.values(); - while (index > 0) { - values.next(); - index--; - } - return values.next().value; - } - - export function checkProjectActualFiles(project: server.Project, expectedFiles: ReadonlyArray) { - checkArray(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); - } - function checkProjectRootFiles(project: server.Project, expectedFiles: ReadonlyArray) { checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); } - function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { - dir = normalizePath(dir); - const result: string[] = []; - forEachAncestorDirectory(dir, ancestor => { - 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); } @@ -533,79 +116,6 @@ namespace ts.projectSystem { return index; } - /** - * Test server cancellation token used to mock host token cancellation requests. - * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls - * should be made before canceling the token. The id of the request to cancel should be set with - * setRequestToCancel(); - */ - export class TestServerCancellationToken implements server.ServerCancellationToken { - private currentId: number | undefined = -1; - private requestToCancel = -1; - private isCancellationRequestedCount = 0; - - constructor(private cancelAfterRequest = 0) { - } - - setRequest(requestId: number) { - this.currentId = requestId; - } - - setRequestToCancel(requestId: number) { - this.resetToken(); - this.requestToCancel = requestId; - } - - resetRequest(requestId: number) { - assert.equal(requestId, this.currentId, "unexpected request id in cancellation"); - this.currentId = undefined; - } - - isCancellationRequested() { - this.isCancellationRequestedCount++; - // If the request id is the request to cancel and isCancellationRequestedCount - // has been met then cancel the request. Ex: cancel the request if it is a - // nav bar request & isCancellationRequested() has already been called three times. - return this.requestToCancel === this.currentId && this.isCancellationRequestedCount >= this.cancelAfterRequest; - } - - resetToken() { - this.currentId = -1; - this.isCancellationRequestedCount = 0; - this.requestToCancel = -1; - } - } - - export function makeSessionRequest(command: string, args: T): protocol.Request { - return { - seq: 0, - type: "request", - command, - arguments: args - }; - } - - export function executeSessionRequest(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] { - return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"]; - } - - export function executeSessionRequestNoResponse(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): void { - session.executeCommand(makeSessionRequest(command, args)); - } - - export function openFilesForSession(files: ReadonlyArray, session: server.Session): void { - for (const file of files) { - session.executeCommand(makeSessionRequest(CommandNames.Open, - "projectRootPath" in file ? { file: typeof file.file === "string" ? file.file : file.file.path, projectRootPath: file.projectRootPath } : { file: file.path })); - } - } - - export function closeFilesForSession(files: ReadonlyArray, session: server.Session): void { - for (const file of files) { - session.executeCommand(makeSessionRequest(CommandNames.Close, { file: file.path })); - } - } - interface ErrorInformation { diagnosticMessage: DiagnosticMessage; errorTextArguments?: string[]; @@ -4171,47 +3681,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem extra resolution pass in lshost", () => { - it("can load typings that are proper modules", () => { - const file1 = { - path: "/a/b/app.js", - content: `var x = require("lib")` - }; - const lib = { - path: "/a/cache/node_modules/@types/lib/index.d.ts", - content: "export let x = 1" - }; - const host: TestServerHost & ModuleResolutionHost = createServerHost([file1, lib]); - const resolutionTrace = createHostModuleResolutionTrace(host); - const projectService = createProjectService(host, { typingsInstaller: new TestTypingsInstaller("/a/cache", /*throttleLimit*/5, host) }); - - projectService.setCompilerOptionsForInferredProjects({ traceResolution: true, allowJs: true }); - projectService.openClientFile(file1.path); - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - const proj = projectService.inferredProjects[0]; - - assert.deepEqual(resolutionTrace, [ - "======== Resolving module 'lib' from '/a/b/app.js'. ========", - "Module resolution kind is not specified, using 'NodeJs'.", - "Loading module 'lib' from 'node_modules' folder, target file type 'TypeScript'.", - "Directory '/a/b/node_modules' does not exist, skipping all lookups in it.", - "Directory '/a/node_modules' does not exist, skipping all lookups in it.", - "Directory '/node_modules' does not exist, skipping all lookups in it.", - "Loading module 'lib' from 'node_modules' folder, target file type 'JavaScript'.", - "Directory '/a/b/node_modules' does not exist, skipping all lookups in it.", - "Directory '/a/node_modules' does not exist, skipping all lookups in it.", - "Directory '/node_modules' does not exist, skipping all lookups in it.", - "======== Module name 'lib' was not resolved. ========", - `Auto discovery for typings is enabled in project '${proj.getProjectName()}'. Running extra resolution pass for module 'lib' using cache location '/a/cache'.`, - "File '/a/cache/node_modules/lib.d.ts' does not exist.", - "File '/a/cache/node_modules/@types/lib/package.json' does not exist.", - "File '/a/cache/node_modules/@types/lib.d.ts' does not exist.", - "File '/a/cache/node_modules/@types/lib/index.d.ts' exist - use it as a name resolution result.", - ]); - checkProjectActualFiles(proj, [file1.path, lib.path]); - }); - }); - describe("tsserverProjectSystem navigate-to for javascript project", () => { function containsNavToItem(items: protocol.NavtoItem[], itemName: string, itemKind: string) { return find(items, item => item.name === itemName && item.kind === itemKind) !== undefined; @@ -8218,56 +7687,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem Watched recursive directories with windows style file system", () => { - function verifyWatchedDirectories(rootedPath: string, useProjectAtRoot: boolean) { - const root = useProjectAtRoot ? rootedPath : `${rootedPath}myfolder/allproject/`; - const configFile: File = { - path: root + "project/tsconfig.json", - content: "{}" - }; - const file1: File = { - path: root + "project/file1.ts", - content: "let x = 10;" - }; - const file2: File = { - 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); - } - - function verifyRootedDirectoryWatch(rootedPath: string) { - it("When project is in rootFolder of style c:/", () => { - verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ true); - }); - - it("When files at some folder other than root", () => { - verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ false); - }); - } - - describe("for rootFolder of style c:/", () => { - verifyRootedDirectoryWatch("c:/"); - }); - - describe("for rootFolder of style c:/users/username", () => { - verifyRootedDirectoryWatch("c:/users/username/"); - }); - }); - describe("tsserverProjectSystem typingsInstaller on inferred Project", () => { it("when projectRootPath is provided", () => { const projects = "/users/username/projects"; @@ -8671,490 +8090,6 @@ new C();` }); }); - describe("tsserverProjectSystem module resolution caching", () => { - const projectLocation = "/user/username/projects/myproject"; - const configFile: File = { - path: `${projectLocation}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { traceResolution: true } }) - }; - - function getModules(module1Path: string, module2Path: string) { - const module1: File = { - path: module1Path, - content: `export function module1() {}` - }; - const module2: File = { - 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: File, 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: File, moduleName: string, useNodeModules: boolean, cacheLocation?: string) { - let foundModule = false; - forEachAncestorDirectory(dirPath, dirPath => { - if (dirPath === cacheLocation) { - return foundModule; - } - - 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: File, moduleName: string) { - expectedTrace.push( - `======== Resolving module '${moduleName}' from '${file.path}'. ========`, - `Module resolution kind is not specified, using 'NodeJs'.` - ); - } - - function getExpectedResolutionTraceFooter(expectedTrace: string[], module: File, moduleName: string, addRealPathTrace: boolean, ignoreModuleFileFound?: boolean) { - if (!ignoreModuleFileFound) { - 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: File, module: File, 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: File, module: File, 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 getExpectedNonRelativeModuleResolutionFromCacheTrace(host: TestServerHost, file: File, module: File, moduleName: string, cacheLocation: 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, cacheLocation); - expectedTrace.push(`Resolution for module '${moduleName}' was found in cache from location '${cacheLocation}'.`); - getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ false, /*ignoreModuleFileFound*/ true); - return expectedTrace; - } - - function getExpectedReusingResolutionFromOldProgram(file: File, moduleName: string) { - return `Reusing resolution of module '${moduleName}' to file '${file.path}' from old program.`; - } - - function verifyWatchesWithConfigFile(host: TestServerHost, files: File[], openFile: File, extraExpectedDirectories?: ReadonlyArray) { - const expectedRecursiveDirectories = arrayToSet([projectLocation, `${projectLocation}/${nodeModulesAtTypes}`, ...(extraExpectedDirectories || emptyArray)]); - checkWatchedFiles(host, mapDefined(files, f => { - if (f === openFile) { - return undefined; - } - const indexOfNodeModules = f.path.indexOf("/node_modules/"); - if (indexOfNodeModules === -1) { - return f.path; - } - expectedRecursiveDirectories.set(f.path.substr(0, indexOfNodeModules + "/node_modules".length), true); - return undefined; - })); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, arrayFrom(expectedRecursiveDirectories.keys()), /*recursive*/ true); - } - - describe("from files in same folder", () => { - function getFiles(fileContent: string) { - const file1: File = { - path: `${projectLocation}/src/file1.ts`, - content: fileContent - }; - const file2: File = { - 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 expectedNonRelativeDirectories = [`${projectLocation}/node_modules`, `${projectLocation}/src`]; - 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, expectedNonRelativeDirectories); - - file1.content += fileContent; - file2.content += fileContent; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - verifyTrace(resolutionTrace, [ - getExpectedReusingResolutionFromOldProgram(file1, module1Name), - getExpectedReusingResolutionFromOldProgram(file1, module2Name) - ]); - verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); - }); - }); - - describe("from files in different folders", () => { - function getFiles(fileContent1: string, fileContent2 = fileContent1, fileContent3 = fileContent1, fileContent4 = fileContent1) { - const file1: File = { - path: `${projectLocation}/product/src/file1.ts`, - content: fileContent1 - }; - const file2: File = { - path: `${projectLocation}/product/src/feature/file2.ts`, - content: fileContent2 - }; - const file3: File = { - path: `${projectLocation}/product/test/src/file3.ts`, - content: fileContent3 - }; - const file4: File = { - 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 expectedNonRelativeDirectories = [`${projectLocation}/node_modules`, `${projectLocation}/product`]; - 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); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module1, module1Name, getDirectoryPath(file1.path), expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module2, module2Name, getDirectoryPath(file1.path), expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module1, module1Name, `${projectLocation}/product`, expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module2, module2Name, `${projectLocation}/product`, expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module1, module1Name, getDirectoryPath(file4.path), expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module2, module2Name, getDirectoryPath(file4.path), expectedTrace); - verifyTrace(resolutionTrace, expectedTrace); - verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); - - 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, expectedNonRelativeDirectories); - }); - - 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); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module1, module1Name, getDirectoryPath(file1.path), expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module2, module2Name, getDirectoryPath(file1.path), expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module1, module1Name, `${projectLocation}/product`, expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module2, module2Name, `${projectLocation}/product`, expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module1, module1Name, getDirectoryPath(file4.path), expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module2, module2Name, getDirectoryPath(file4.path), expectedTrace); - verifyTrace(resolutionTrace, expectedTrace); - - const currentDirectory = getDirectoryPath(file1.path); - const watchedFiles = mapDefined(files, f => f === file1 || f.path.indexOf("/node_modules/") !== -1 ? undefined : f.path); - forEachAncestorDirectory(currentDirectory, d => { - watchedFiles.push(combinePaths(d, "tsconfig.json"), combinePaths(d, "jsconfig.json")); - }); - const watchedRecursiveDirectories = getTypeRootsFromLocation(currentDirectory).concat([ - `${currentDirectory}/node_modules`, `${currentDirectory}/feature`, `${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); - } - }); - }); - - describe("when watching directories for failed lookup locations in amd resolution", () => { - const projectRoot = "/user/username/projects/project"; - const nodeFile: File = { - path: `${projectRoot}/src/typings/node.d.ts`, - content: ` -declare module "fs" { - export interface something { - } -}` - }; - const electronFile: File = { - path: `${projectRoot}/src/typings/electron.d.ts`, - content: ` -declare module 'original-fs' { - import * as fs from 'fs'; - export = fs; -}` - }; - const srcFile: File = { - path: `${projectRoot}/src/somefolder/srcfile.ts`, - content: ` -import { x } from "somefolder/module1"; -import { x } from "somefolder/module2"; -const y = x;` - }; - const moduleFile: File = { - path: `${projectRoot}/src/somefolder/module1.ts`, - content: ` -export const x = 10;` - }; - const configFile: File = { - path: `${projectRoot}/src/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "amd", - moduleResolution: "classic", - target: "es5", - outDir: "../out", - baseUrl: "./", - typeRoots: ["typings"] - } - }) - }; - - function verifyModuleResolution(useNodeFile: boolean) { - const files = [...(useNodeFile ? [nodeFile] : []), electronFile, srcFile, moduleFile, configFile, libFile]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(srcFile.path, srcFile.content, ScriptKind.TS, projectRoot); - checkProjectActualFiles(service.configuredProjects.get(configFile.path)!, files.map(f => f.path)); - checkWatchedFilesDetailed(host, mapDefined(files, f => f === srcFile ? undefined : f.path), 1); - if (useNodeFile) { - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); // since fs resolves to ambient module, shouldnt watch failed lookup - } - else { - checkWatchedDirectoriesDetailed(host, [`${projectRoot}`, `${projectRoot}/src`], 1, /*recursive*/ false); // failed lookup for fs - } - const expectedWatchedDirectories = createMap(); - expectedWatchedDirectories.set(`${projectRoot}/src`, 1); // Wild card - expectedWatchedDirectories.set(`${projectRoot}/src/somefolder`, 1); // failedLookup for somefolder/module2 - expectedWatchedDirectories.set(`${projectRoot}/src/node_modules`, 1); // failed lookup for somefolder/module2 - expectedWatchedDirectories.set(`${projectRoot}/somefolder`, 1); // failed lookup for somefolder/module2 - expectedWatchedDirectories.set(`${projectRoot}/node_modules`, 1); // failed lookup for with node_modules/@types/fs - expectedWatchedDirectories.set(`${projectRoot}/src/typings`, useNodeFile ? 1 : 2); // typeroot directory + failed lookup if not using node file - checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, /*recursive*/ true); - } - - it("when resolves to ambient module", () => { - verifyModuleResolution(/*useNodeFile*/ true); - }); - - it("when resolution fails", () => { - verifyModuleResolution(/*useNodeFile*/ false); - }); - }); - - describe("ignores files/folder changes in node_modules that start with '.'", () => { - const projectPath = "/user/username/projects/project"; - const npmCacheFile: File = { - path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, - content: JSON.stringify({ something: 10 }) - }; - const file1: File = { - path: `${projectPath}/test.ts`, - content: `import { x } from "somemodule";` - }; - const file2: File = { - path: `${projectPath}/node_modules/somemodule/index.d.ts`, - content: `export const x = 10;` - }; - it("when watching node_modules in inferred project for failed lookup/closed script infos", () => { - const files = [libFile, file1, file2]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(file1.path); - checkNumberOfProjects(service, { inferredProjects: 1 }); - const project = service.inferredProjects[0]; - checkProjectActualFiles(project, files.map(f => f.path)); - (project as ResolutionCacheHost).maxNumberOfFilesToIterateForInvalidation = 1; - host.checkTimeoutQueueLength(0); - - host.ensureFileOrFolder(npmCacheFile); - host.checkTimeoutQueueLength(0); - }); - it("when watching node_modules as part of wild card directories in config project", () => { - const config: File = { - path: `${projectPath}/tsconfig.json`, - content: "{}" - }; - const files = [libFile, file1, file2, config]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(file1.path); - checkNumberOfProjects(service, { configuredProjects: 1 }); - const project = Debug.assertDefined(service.configuredProjects.get(config.path)); - checkProjectActualFiles(project, files.map(f => f.path)); - host.checkTimeoutQueueLength(0); - - host.ensureFileOrFolder(npmCacheFile); - host.checkTimeoutQueueLength(0); - }); - }); - }); - describe("tsserverProjectSystem getEditsForFileRename", () => { it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => { const userTs: File = { diff --git a/src/testRunner/unittests/typingsInstaller.ts b/src/testRunner/unittests/typingsInstaller.ts index 34c5fa2a413..2b18af6a71f 100644 --- a/src/testRunner/unittests/typingsInstaller.ts +++ b/src/testRunner/unittests/typingsInstaller.ts @@ -1,5 +1,4 @@ namespace ts.projectSystem { - import TI = server.typingsInstaller; import validatePackageName = JsTyping.validatePackageName; import PackageNameValidationResult = JsTyping.PackageNameValidationResult; diff --git a/src/testRunner/unittests/watchApi.ts b/src/testRunner/unittests/watchApi.ts new file mode 100644 index 00000000000..60072e24730 --- /dev/null +++ b/src/testRunner/unittests/watchApi.ts @@ -0,0 +1,40 @@ +namespace ts.tscWatch { + describe("watchAPI:: tsc-watch with custom module resolution", () => { + const projectRoot = "/user/username/projects/project"; + const configFileJson: any = { + compilerOptions: { module: "commonjs", resolveJsonModule: true }, + files: ["index.ts"] + }; + const mainFile: File = { + path: `${projectRoot}/index.ts`, + content: "import settings from './settings.json';" + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify(configFileJson) + }; + const settingsJson: File = { + path: `${projectRoot}/settings.json`, + content: JSON.stringify({ content: "Print this" }) + }; + + it("verify that module resolution with json extension works when returned without extension", () => { + const files = [libFile, mainFile, config, settingsJson]; + const host = createWatchedSystem(files, { currentDirectory: projectRoot }); + const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, host); + const parsedCommandResult = parseJsonConfigFileContent(configFileJson, host, config.path); + compilerHost.resolveModuleNames = (moduleNames, containingFile) => moduleNames.map(m => { + const result = resolveModuleName(m, containingFile, parsedCommandResult.options, compilerHost); + const resolvedModule = result.resolvedModule!; + return { + resolvedFileName: resolvedModule.resolvedFileName, + isExternalLibraryImport: resolvedModule.isExternalLibraryImport, + originalFileName: resolvedModule.originalPath, + }; + }); + const watch = createWatchProgram(compilerHost); + const program = watch.getCurrentProgram().getProgram(); + checkProgramActualFiles(program, [mainFile.path, libFile.path, settingsJson.path]); + }); + }); +} diff --git a/src/testRunner/unittests/watchEnvironment.ts b/src/testRunner/unittests/watchEnvironment.ts index 5b620b54e85..74a6637d6fc 100644 --- a/src/testRunner/unittests/watchEnvironment.ts +++ b/src/testRunner/unittests/watchEnvironment.ts @@ -262,5 +262,55 @@ namespace ts { verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); }); }); + + describe("watchEnvironment:: tsserverProjectSystem Watched recursive directories with windows style file system", () => { + function verifyWatchedDirectories(rootedPath: string, useProjectAtRoot: boolean) { + const root = useProjectAtRoot ? rootedPath : `${rootedPath}myfolder/allproject/`; + const configFile: File = { + path: root + "project/tsconfig.json", + content: "{}" + }; + const file1: File = { + path: root + "project/file1.ts", + content: "let x = 10;" + }; + const file2: File = { + 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); + } + + function verifyRootedDirectoryWatch(rootedPath: string) { + it("When project is in rootFolder of style c:/", () => { + verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ true); + }); + + it("When files at some folder other than root", () => { + verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ false); + }); + } + + describe("for rootFolder of style c:/", () => { + verifyRootedDirectoryWatch("c:/"); + }); + + describe("for rootFolder of style c:/users/username", () => { + verifyRootedDirectoryWatch("c:/users/username/"); + }); + }); } } From 347f89c8516f755cc47849f5fa90517d2893707a Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 10:52:00 -0800 Subject: [PATCH 04/20] tsc-watch emit tests in to its own tests --- src/testRunner/tsconfig.json | 1 + src/testRunner/unittests/tscWatchEmit.ts | 718 +++++++++++++++++++ src/testRunner/unittests/tscWatchHelpers.ts | 66 +- src/testRunner/unittests/tscWatchMode.ts | 729 -------------------- 4 files changed, 720 insertions(+), 794 deletions(-) create mode 100644 src/testRunner/unittests/tscWatchEmit.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 674188eee63..a95bc106fb1 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -86,6 +86,7 @@ "unittests/tsbuild.ts", "unittests/tsbuildWatchMode.ts", "unittests/tsconfigParsing.ts", + "unittests/tscWatchEmit.ts", "unittests/tscWatchMode.ts", "unittests/tsserverProjectSystem.ts", "unittests/typingsInstaller.ts", diff --git a/src/testRunner/unittests/tscWatchEmit.ts b/src/testRunner/unittests/tscWatchEmit.ts new file mode 100644 index 00000000000..e0e9abe516c --- /dev/null +++ b/src/testRunner/unittests/tscWatchEmit.ts @@ -0,0 +1,718 @@ +namespace ts.tscWatch { + function getEmittedLineForMultiFileOutput(file: File, host: WatchedSystem) { + return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; + } + + function getEmittedLineForSingleFileOutput(filename: string, host: WatchedSystem) { + return `TSFILE: ${filename}${host.newLine}`; + } + + interface FileOrFolderEmit extends File { + output?: string; + } + + function getFileOrFolderEmit(file: File, getOutput?: (file: File) => string): FileOrFolderEmit { + const result = file as FileOrFolderEmit; + if (getOutput) { + result.output = getOutput(file); + } + return result; + } + + function getEmittedLines(files: FileOrFolderEmit[]) { + const seen = createMap(); + const result: string[] = []; + for (const { output } of files) { + if (output && !seen.has(output)) { + seen.set(output, true); + result.push(output); + } + } + return result; + } + + function checkAffectedLines(host: WatchedSystem, affectedFiles: FileOrFolderEmit[], allEmittedFiles: string[]) { + const expectedAffectedFiles = getEmittedLines(affectedFiles); + const expectedNonAffectedFiles = mapDefined(allEmittedFiles, line => contains(expectedAffectedFiles, line) ? undefined : line); + checkOutputContains(host, expectedAffectedFiles); + checkOutputDoesNotContain(host, expectedNonAffectedFiles); + } + + describe("tsc-watch emit with outFile or out setting", () => { + function createWatchForOut(out?: string, outFile?: string) { + const host = createWatchedSystem([]); + const config: FileOrFolderEmit = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { listEmittedFiles: true } + }) + }; + + let getOutput: (file: File) => string; + if (out) { + config.content = JSON.stringify({ + compilerOptions: { listEmittedFiles: true, out } + }); + getOutput = __ => getEmittedLineForSingleFileOutput(out, host); + } + else if (outFile) { + config.content = JSON.stringify({ + compilerOptions: { listEmittedFiles: true, outFile } + }); + getOutput = __ => getEmittedLineForSingleFileOutput(outFile, host); + } + else { + getOutput = file => getEmittedLineForMultiFileOutput(file, host); + } + + const f1 = getFileOrFolderEmit({ + path: "/a/a.ts", + content: "let x = 1" + }, getOutput); + const f2 = getFileOrFolderEmit({ + path: "/a/b.ts", + content: "let y = 1" + }, getOutput); + + const files = [f1, f2, config, libFile]; + host.reloadFS(files); + createWatchOfConfigFile(config.path, host); + + const allEmittedLines = getEmittedLines(files); + checkOutputContains(host, allEmittedLines); + host.clearOutput(); + + f1.content = "let x = 11"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkAffectedLines(host, [f1], allEmittedLines); + } + + it("projectUsesOutFile should not be returned if not set", () => { + createWatchForOut(); + }); + + it("projectUsesOutFile should be true if out is set", () => { + const outJs = "/a/out.js"; + createWatchForOut(outJs); + }); + + it("projectUsesOutFile should be true if outFile is set", () => { + const outJs = "/a/out.js"; + createWatchForOut(/*out*/ undefined, outJs); + }); + + function verifyFilesEmittedOnce(useOutFile: boolean) { + const file1: File = { + path: "/a/b/output/AnotherDependency/file1.d.ts", + content: "declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }" + }; + const file2: File = { + path: "/a/b/dependencies/file2.d.ts", + content: "declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }" + }; + const file3: File = { + path: "/a/b/project/src/main.ts", + content: "namespace Main { export function fooBar() {} }" + }; + const file4: File = { + path: "/a/b/project/src/main2.ts", + content: "namespace main.file4 { import DynamicMenu = Common.SomeComponent.DynamicMenu; export function foo(a: DynamicMenu.z) { } }" + }; + const configFile: File = { + path: "/a/b/project/tsconfig.json", + content: JSON.stringify({ + compilerOptions: useOutFile ? + { outFile: "../output/common.js", target: "es5" } : + { outDir: "../output", 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); + const mapOfFilesWritten = createMap(); + host.writeFile = (p: string, content: string) => { + const count = mapOfFilesWritten.get(p); + mapOfFilesWritten.set(p, count ? count + 1 : 1); + return originalWriteFile(p, content); + }; + createWatchOfConfigFile(configFile.path, host); + 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); + }); + }); + + describe("tsc-watch emit for configured projects", () => { + const file1Consumer1Path = "/a/b/file1Consumer1.ts"; + const moduleFile1Path = "/a/b/moduleFile1.ts"; + const configFilePath = "/a/b/tsconfig.json"; + interface InitialStateParams { + /** custom config file options */ + configObj?: any; + /** list of the files that will be emitted for first compilation */ + firstCompilationEmitFiles?: string[]; + /** get the emit file for file - default is multi file emit line */ + getEmitLine?(file: File, host: WatchedSystem): string; + /** Additional files and folders to add */ + getAdditionalFileOrFolder?(): File[]; + /** initial list of files to emit if not the default list */ + firstReloadFileList?: string[]; + } + function getInitialState({ configObj = {}, firstCompilationEmitFiles, getEmitLine, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { + const host = createWatchedSystem([]); + const getOutputName = getEmitLine ? (file: File) => getEmitLine(file, host) : + (file: File) => getEmittedLineForMultiFileOutput(file, host); + + const moduleFile1 = getFileOrFolderEmit({ + path: moduleFile1Path, + content: "export function Foo() { };", + }, getOutputName); + + const file1Consumer1 = getFileOrFolderEmit({ + path: file1Consumer1Path, + content: `import {Foo} from "./moduleFile1"; export var y = 10;`, + }, getOutputName); + + const file1Consumer2 = getFileOrFolderEmit({ + path: "/a/b/file1Consumer2.ts", + content: `import {Foo} from "./moduleFile1"; let z = 10;`, + }, getOutputName); + + const moduleFile2 = getFileOrFolderEmit({ + path: "/a/b/moduleFile2.ts", + content: `export var Foo4 = 10;`, + }, getOutputName); + + const globalFile3 = getFileOrFolderEmit({ + path: "/a/b/globalFile3.ts", + content: `interface GlobalFoo { age: number }` + }); + + const additionalFiles = getAdditionalFileOrFolder ? + map(getAdditionalFileOrFolder(), file => getFileOrFolderEmit(file, getOutputName)) : + []; + + (configObj.compilerOptions || (configObj.compilerOptions = {})).listEmittedFiles = true; + const configFile = getFileOrFolderEmit({ + path: configFilePath, + content: JSON.stringify(configObj) + }); + + const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile, ...additionalFiles]; + let allEmittedFiles = getEmittedLines(files); + host.reloadFS(firstReloadFileList ? getFiles(firstReloadFileList) : files); + + // Initial compile + createWatchOfConfigFile(configFile.path, host); + if (firstCompilationEmitFiles) { + checkAffectedLines(host, getFiles(firstCompilationEmitFiles), allEmittedFiles); + } + else { + checkOutputContains(host, allEmittedFiles); + } + host.clearOutput(); + + return { + moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, + files, + getFile, + verifyAffectedFiles, + verifyAffectedAllFiles, + getOutputName + }; + + function getFiles(filelist: string[]) { + return map(filelist, getFile); + } + + function getFile(fileName: string) { + return find(files, file => file.path === fileName)!; + } + + function verifyAffectedAllFiles() { + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(1); + checkOutputContains(host, allEmittedFiles); + host.clearOutput(); + } + + function verifyAffectedFiles(expected: FileOrFolderEmit[], filesToReload?: FileOrFolderEmit[]) { + if (!filesToReload) { + filesToReload = files; + } + else if (filesToReload.length > files.length) { + allEmittedFiles = getEmittedLines(filesToReload); + } + host.reloadFS(filesToReload); + host.checkTimeoutQueueLengthAndRun(1); + checkAffectedLines(host, expected, allEmittedFiles); + host.clearOutput(); + } + } + + it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + verifyAffectedFiles + } = getInitialState(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` + moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; + verifyAffectedFiles([moduleFile1]); + }); + + it("should be up-to-date with the reference map changes", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + verifyAffectedFiles + } = getInitialState(); + + // Change file1Consumer1 content to `export let y = Foo();` + file1Consumer1.content = `export let y = Foo();`; + verifyAffectedFiles([file1Consumer1]); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer2]); + + // Add the import statements back to file1Consumer1 + file1Consumer1.content = `import {Foo} from "./moduleFile1";let y = Foo();`; + verifyAffectedFiles([file1Consumer1]); + + // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` + moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer2, file1Consumer1]); + + // Multiple file edits in one go: + + // Change file1Consumer1 content to `export let y = Foo();` + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + file1Consumer1.content = `export let y = Foo();`; + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); + }); + + it("should be up-to-date with deleted files", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + files, + verifyAffectedFiles + } = getInitialState(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + + // Delete file1Consumer2 + const filesToLoad = mapDefined(files, file => file === file1Consumer2 ? undefined : file); + verifyAffectedFiles([moduleFile1, file1Consumer1], filesToLoad); + }); + + it("should be up-to-date with newly created files", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + files, + verifyAffectedFiles, + getOutputName + } = getInitialState(); + + const file1Consumer3 = getFileOrFolderEmit({ + path: "/a/b/file1Consumer3.ts", + content: `import {Foo} from "./moduleFile1"; let y = Foo();` + }, getOutputName); + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer3, file1Consumer2], files.concat(file1Consumer3)); + }); + + it("should detect changes in non-root files", () => { + const { + moduleFile1, file1Consumer1, + verifyAffectedFiles + } = getInitialState({ configObj: { files: [file1Consumer1Path] }, firstCompilationEmitFiles: [file1Consumer1Path, moduleFile1Path] }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1]); + + // change file1 internal, and verify only file1 is affected + moduleFile1.content += "var T1: number;"; + verifyAffectedFiles([moduleFile1]); + }); + + it("should return all files if a global file changed shape", () => { + const { + globalFile3, verifyAffectedAllFiles + } = getInitialState(); + + globalFile3.content += "var T2: string;"; + verifyAffectedAllFiles(); + }); + + it("should always return the file itself if '--isolatedModules' is specified", () => { + const { + moduleFile1, verifyAffectedFiles + } = getInitialState({ configObj: { compilerOptions: { isolatedModules: true } } }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1]); + }); + + it("should always return the file itself if '--out' or '--outFile' is specified", () => { + const outFilePath = "/a/b/out.js"; + const { + moduleFile1, verifyAffectedFiles + } = getInitialState({ + configObj: { compilerOptions: { module: "system", outFile: outFilePath } }, + getEmitLine: (_, host) => getEmittedLineForSingleFileOutput(outFilePath, host) + }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1]); + }); + + it("should return cascaded affected file list", () => { + const file1Consumer1Consumer1: File = { + path: "/a/b/file1Consumer1Consumer1.ts", + content: `import {y} from "./file1Consumer1";` + }; + const { + moduleFile1, file1Consumer1, file1Consumer2, verifyAffectedFiles, getFile + } = getInitialState({ + getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] + }); + + const file1Consumer1Consumer1Emit = getFile(file1Consumer1Consumer1.path); + file1Consumer1.content += "export var T: number;"; + verifyAffectedFiles([file1Consumer1, file1Consumer1Consumer1Emit]); + + // Doesnt change the shape of file1Consumer1 + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); + + // Change both files before the timeout + file1Consumer1.content += "export var T2: number;"; + moduleFile1.content = `export var T2: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer1Consumer1Emit]); + }); + + it("should work fine for files with circular references", () => { + // TODO: do not exit on such errors? Just continue to watch the files for update in watch mode + + const file1: File = { + path: "/a/b/file1.ts", + content: ` + /// + export var t1 = 10;` + }; + const file2: File = { + path: "/a/b/file2.ts", + content: ` + /// + export var t2 = 10;` + }; + const { + configFile, + getFile, + verifyAffectedFiles + } = getInitialState({ + firstCompilationEmitFiles: [file1.path, file2.path], + getAdditionalFileOrFolder: () => [file1, file2], + firstReloadFileList: [libFile.path, file1.path, file2.path, configFilePath] + }); + const file1Emit = getFile(file1.path), file2Emit = getFile(file2.path); + + file1Emit.content += "export var t3 = 10;"; + verifyAffectedFiles([file1Emit, file2Emit], [file1, file2, libFile, configFile]); + + }); + + it("should detect removed code file", () => { + const referenceFile1: File = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const { + configFile, + getFile, + verifyAffectedFiles + } = getInitialState({ + firstCompilationEmitFiles: [referenceFile1.path, moduleFile1Path], + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [libFile.path, referenceFile1.path, moduleFile1Path, configFilePath] + }); + + const referenceFile1Emit = getFile(referenceFile1.path); + verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]); + }); + + it("should detect non-existing code file", () => { + const referenceFile1: File = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const { + configFile, + moduleFile2, + getFile, + verifyAffectedFiles + } = getInitialState({ + firstCompilationEmitFiles: [referenceFile1.path], + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [libFile.path, referenceFile1.path, configFilePath] + }); + + const referenceFile1Emit = getFile(referenceFile1.path); + referenceFile1Emit.content += "export var yy = Foo();"; + verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]); + + // Create module File2 and see both files are saved + verifyAffectedFiles([referenceFile1Emit, moduleFile2], [libFile, moduleFile2, referenceFile1Emit, configFile]); + }); + }); + + describe("tsc-watch emit file content", () => { + interface EmittedFile extends File { + shouldBeWritten: boolean; + } + function getEmittedFiles(files: FileOrFolderEmit[], contents: string[]): EmittedFile[] { + return map(contents, (content, index) => { + return { + content, + path: changeExtension(files[index].path, Extension.Js), + shouldBeWritten: true + }; + } + ); + } + function verifyEmittedFiles(host: WatchedSystem, emittedFiles: EmittedFile[]) { + for (const { path, content, shouldBeWritten } of emittedFiles) { + if (shouldBeWritten) { + assert.isTrue(host.fileExists(path), `Expected file ${path} to be present`); + assert.equal(host.readFile(path), content, `Contents of file ${path} do not match`); + } + else { + assert.isNotTrue(host.fileExists(path), `Expected file ${path} to be absent`); + } + } + } + + function verifyEmittedFileContents(newLine: string, inputFiles: File[], initialEmittedFileContents: string[], + modifyFiles: (files: FileOrFolderEmit[], emitedFiles: EmittedFile[]) => FileOrFolderEmit[], configFile?: File) { + const host = createWatchedSystem([], { newLine }); + const files = concatenate( + map(inputFiles, file => getFileOrFolderEmit(file, fileToConvert => getEmittedLineForMultiFileOutput(fileToConvert, host))), + configFile ? [libFile, configFile] : [libFile] + ); + const allEmittedFiles = getEmittedLines(files); + host.reloadFS(files); + + // Initial compile + if (configFile) { + createWatchOfConfigFile(configFile.path, host); + } + else { + // First file as the root + createWatchOfFilesAndCompilerOptions([files[0].path], host, { listEmittedFiles: true }); + } + checkOutputContains(host, allEmittedFiles); + + const emittedFiles = getEmittedFiles(files, initialEmittedFileContents); + verifyEmittedFiles(host, emittedFiles); + host.clearOutput(); + + const affectedFiles = modifyFiles(files, emittedFiles); + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(1); + checkAffectedLines(host, affectedFiles, allEmittedFiles); + + verifyEmittedFiles(host, emittedFiles); + } + + function verifyNewLine(newLine: string) { + const lines = ["var x = 1;", "var y = 2;"]; + const fileContent = lines.join(newLine); + const f = { + path: "/a/app.ts", + content: fileContent + }; + + verifyEmittedFileContents(newLine, [f], [fileContent + newLine], modifyFiles); + + function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { + files[0].content = fileContent + newLine + "var z = 3;"; + emittedFiles[0].content = files[0].content + newLine; + return [files[0]]; + } + } + + it("handles new lines: \\n", () => { + verifyNewLine("\n"); + }); + + it("handles new lines: \\r\\n", () => { + verifyNewLine("\r\n"); + }); + + it("should emit specified file", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export function Foo() { return 10; }` + }; + + const file2 = { + path: "/a/b/f2.ts", + content: `import {Foo} from "./f1"; export let y = Foo();` + }; + + const file3 = { + path: "/a/b/f3.ts", + content: `import {y} from "./f2"; let x = y;` + }; + + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { listEmittedFiles: true } }) + }; + + verifyEmittedFileContents("\r\n", [file1, file2, file3], [ + `"use strict";\r\nexports.__esModule = true;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`, + `"use strict";\r\nexports.__esModule = true;\r\nvar f1_1 = require("./f1");\r\nexports.y = f1_1.Foo();\r\n`, + `"use strict";\r\nexports.__esModule = true;\r\nvar f2_1 = require("./f2");\r\nvar x = f2_1.y;\r\n` + ], modifyFiles, configFile); + + function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { + files[0].content += `export function foo2() { return 2; }`; + emittedFiles[0].content += `function foo2() { return 2; }\r\nexports.foo2 = foo2;\r\n`; + emittedFiles[2].shouldBeWritten = false; + return files.slice(0, 2); + } + }); + + it("Elides const enums correctly in incremental compilation", () => { + const currentDirectory = "/user/someone/projects/myproject"; + const file1: File = { + path: `${currentDirectory}/file1.ts`, + content: "export const enum E1 { V = 1 }" + }; + const file2: File = { + path: `${currentDirectory}/file2.ts`, + content: `import { E1 } from "./file1"; export const enum E2 { V = E1.V }` + }; + const file3: File = { + path: `${currentDirectory}/file3.ts`, + content: `import { E2 } from "./file2"; const v: E2 = E2.V;` + }; + const strictAndEsModule = `"use strict";\nexports.__esModule = true;\n`; + verifyEmittedFileContents("\n", [file3, file2, file1], [ + `${strictAndEsModule}var v = 1 /* V */;\n`, + strictAndEsModule, + strictAndEsModule + ], modifyFiles); + + function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { + files[0].content += `function foo2() { return 2; }`; + emittedFiles[0].content += `function foo2() { return 2; }\n`; + emittedFiles[1].shouldBeWritten = false; + emittedFiles[2].shouldBeWritten = false; + return [files[0]]; + } + }); + + it("file is deleted and created as part of change", () => { + const projectLocation = "/home/username/project"; + const file: File = { + path: `${projectLocation}/app/file.ts`, + content: "var a = 10;" + }; + const fileJs = `${projectLocation}/app/file.js`; + const configFile: File = { + path: `${projectLocation}/tsconfig.json`, + content: JSON.stringify({ + include: [ + "app/**/*.ts" + ] + }) + }; + const files = [file, configFile, libFile]; + const host = createWatchedSystem(files, { currentDirectory: projectLocation, useCaseSensitiveFileNames: true }); + createWatchOfConfigFile("tsconfig.json", host); + verifyProgram(); + + file.content += "\nvar b = 10;"; + + host.reloadFS(files, { invokeFileDeleteCreateAsPartInsteadOfChange: true }); + host.runQueuedTimeoutCallbacks(); + verifyProgram(); + + function verifyProgram() { + assert.isTrue(host.fileExists(fileJs)); + assert.equal(host.readFile(fileJs), file.content + "\n"); + } + }); + }); + + describe("tsc-watch with when module emit is specified as node", () => { + it("when instead of filechanged recursive directory watcher is invoked", () => { + const configFile: File = { + 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: File = { + path: "/a/rootFolder/project/Scripts/TypeScript.ts", + content: "var z = 10;" + }; + const file2: File = { + path: "/a/rootFolder/project/Scripts/Javascript.js", + content: "var zz = 10;" + }; + const files = [configFile, file1, file2, libFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfConfigFile(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/testRunner/unittests/tscWatchHelpers.ts b/src/testRunner/unittests/tscWatchHelpers.ts index 6e3fd40c495..f51ecc4e64d 100644 --- a/src/testRunner/unittests/tscWatchHelpers.ts +++ b/src/testRunner/unittests/tscWatchHelpers.ts @@ -41,45 +41,6 @@ namespace ts.tscWatch { return () => watch.getCurrentProgram().getProgram(); } - //function getEmittedLineForMultiFileOutput(file: File, host: WatchedSystem) { - // return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; - //} - - //function getEmittedLineForSingleFileOutput(filename: string, host: WatchedSystem) { - // return `TSFILE: ${filename}${host.newLine}`; - //} - - //interface FileOrFolderEmit extends File { - // output?: string; - //} - - //function getFileOrFolderEmit(file: File, getOutput?: (file: File) => string): FileOrFolderEmit { - // const result = file as FileOrFolderEmit; - // if (getOutput) { - // result.output = getOutput(file); - // } - // return result; - //} - - //function getEmittedLines(files: FileOrFolderEmit[]) { - // const seen = createMap(); - // const result: string[] = []; - // for (const { output } of files) { - // if (output && !seen.has(output)) { - // seen.set(output, true); - // result.push(output); - // } - // } - // return result; - //} - - //function checkAffectedLines(host: WatchedSystem, affectedFiles: FileOrFolderEmit[], allEmittedFiles: string[]) { - // const expectedAffectedFiles = getEmittedLines(affectedFiles); - // const expectedNonAffectedFiles = mapDefined(allEmittedFiles, line => contains(expectedAffectedFiles, line) ? undefined : line); - // checkOutputContains(host, expectedAffectedFiles); - // checkOutputDoesNotContain(host, expectedNonAffectedFiles); - //} - const elapsedRegex = /^Elapsed:: [0-9]+ms/; function checkOutputErrors( host: WatchedSystem, @@ -182,7 +143,7 @@ namespace ts.tscWatch { assert.equal(host.exitCode, expectedExitCode); } - function getDiagnosticOfFileFrom(file: SourceFile | undefined, text: string, start: number | undefined, length: number | undefined, message: DiagnosticMessage): Diagnostic { + export function getDiagnosticOfFileFrom(file: SourceFile | undefined, text: string, start: number | undefined, length: number | undefined, message: DiagnosticMessage): Diagnostic { return { file, start, @@ -194,31 +155,6 @@ namespace ts.tscWatch { }; } - //function getDiagnosticWithoutFile(message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { - // let text = getLocaleSpecificMessage(message); - - // if (arguments.length > 1) { - // text = formatStringFromArgs(text, arguments, 1); - // } - - // return getDiagnosticOfFileFrom(/*file*/ undefined, text, /*start*/ undefined, /*length*/ undefined, message); - //} - - //function getDiagnosticOfFile(file: SourceFile, start: number, length: number, message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { - // let text = getLocaleSpecificMessage(message); - - // if (arguments.length > 4) { - // text = formatStringFromArgs(text, arguments, 4); - // } - - // return getDiagnosticOfFileFrom(file, text, start, length, message); - //} - - //function getUnknownCompilerOption(program: Program, configFile: File, option: string) { - // const quotedOption = `"${option}"`; - // return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); - //} - export function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { let text = getLocaleSpecificMessage(message); diff --git a/src/testRunner/unittests/tscWatchMode.ts b/src/testRunner/unittests/tscWatchMode.ts index 662f26e8b13..29726b12424 100644 --- a/src/testRunner/unittests/tscWatchMode.ts +++ b/src/testRunner/unittests/tscWatchMode.ts @@ -1,55 +1,4 @@ namespace ts.tscWatch { - function getEmittedLineForMultiFileOutput(file: File, host: WatchedSystem) { - return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; - } - - function getEmittedLineForSingleFileOutput(filename: string, host: WatchedSystem) { - return `TSFILE: ${filename}${host.newLine}`; - } - - interface FileOrFolderEmit extends File { - output?: string; - } - - function getFileOrFolderEmit(file: File, getOutput?: (file: File) => string): FileOrFolderEmit { - const result = file as FileOrFolderEmit; - if (getOutput) { - result.output = getOutput(file); - } - return result; - } - - function getEmittedLines(files: FileOrFolderEmit[]) { - const seen = createMap(); - const result: string[] = []; - for (const { output } of files) { - if (output && !seen.has(output)) { - seen.set(output, true); - result.push(output); - } - } - return result; - } - - function checkAffectedLines(host: WatchedSystem, affectedFiles: FileOrFolderEmit[], allEmittedFiles: string[]) { - const expectedAffectedFiles = getEmittedLines(affectedFiles); - const expectedNonAffectedFiles = mapDefined(allEmittedFiles, line => contains(expectedAffectedFiles, line) ? undefined : line); - checkOutputContains(host, expectedAffectedFiles); - checkOutputDoesNotContain(host, expectedNonAffectedFiles); - } - - function getDiagnosticOfFileFrom(file: SourceFile | undefined, text: string, start: number | undefined, length: number | undefined, message: DiagnosticMessage): Diagnostic { - return { - file, - start, - length, - - messageText: text, - category: message.category, - code: message.code, - }; - } - function getDiagnosticWithoutFile(message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { let text = getLocaleSpecificMessage(message); @@ -1546,684 +1495,6 @@ interface Document { }); }); - describe("tsc-watch emit with outFile or out setting", () => { - function createWatchForOut(out?: string, outFile?: string) { - const host = createWatchedSystem([]); - const config: FileOrFolderEmit = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { listEmittedFiles: true } - }) - }; - - let getOutput: (file: File) => string; - if (out) { - config.content = JSON.stringify({ - compilerOptions: { listEmittedFiles: true, out } - }); - getOutput = __ => getEmittedLineForSingleFileOutput(out, host); - } - else if (outFile) { - config.content = JSON.stringify({ - compilerOptions: { listEmittedFiles: true, outFile } - }); - getOutput = __ => getEmittedLineForSingleFileOutput(outFile, host); - } - else { - getOutput = file => getEmittedLineForMultiFileOutput(file, host); - } - - const f1 = getFileOrFolderEmit({ - path: "/a/a.ts", - content: "let x = 1" - }, getOutput); - const f2 = getFileOrFolderEmit({ - path: "/a/b.ts", - content: "let y = 1" - }, getOutput); - - const files = [f1, f2, config, libFile]; - host.reloadFS(files); - createWatchOfConfigFile(config.path, host); - - const allEmittedLines = getEmittedLines(files); - checkOutputContains(host, allEmittedLines); - host.clearOutput(); - - f1.content = "let x = 11"; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkAffectedLines(host, [f1], allEmittedLines); - } - - it("projectUsesOutFile should not be returned if not set", () => { - createWatchForOut(); - }); - - it("projectUsesOutFile should be true if out is set", () => { - const outJs = "/a/out.js"; - createWatchForOut(outJs); - }); - - it("projectUsesOutFile should be true if outFile is set", () => { - const outJs = "/a/out.js"; - createWatchForOut(/*out*/ undefined, outJs); - }); - - function verifyFilesEmittedOnce(useOutFile: boolean) { - const file1: File = { - path: "/a/b/output/AnotherDependency/file1.d.ts", - content: "declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }" - }; - const file2: File = { - path: "/a/b/dependencies/file2.d.ts", - content: "declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }" - }; - const file3: File = { - path: "/a/b/project/src/main.ts", - content: "namespace Main { export function fooBar() {} }" - }; - const file4: File = { - path: "/a/b/project/src/main2.ts", - content: "namespace main.file4 { import DynamicMenu = Common.SomeComponent.DynamicMenu; export function foo(a: DynamicMenu.z) { } }" - }; - const configFile: File = { - path: "/a/b/project/tsconfig.json", - content: JSON.stringify({ - compilerOptions: useOutFile ? - { outFile: "../output/common.js", target: "es5" } : - { outDir: "../output", 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); - const mapOfFilesWritten = createMap(); - host.writeFile = (p: string, content: string) => { - const count = mapOfFilesWritten.get(p); - mapOfFilesWritten.set(p, count ? count + 1 : 1); - return originalWriteFile(p, content); - }; - createWatchOfConfigFile(configFile.path, host); - 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); - }); - }); - - describe("tsc-watch emit for configured projects", () => { - const file1Consumer1Path = "/a/b/file1Consumer1.ts"; - const moduleFile1Path = "/a/b/moduleFile1.ts"; - const configFilePath = "/a/b/tsconfig.json"; - interface InitialStateParams { - /** custom config file options */ - configObj?: any; - /** list of the files that will be emitted for first compilation */ - firstCompilationEmitFiles?: string[]; - /** get the emit file for file - default is multi file emit line */ - getEmitLine?(file: File, host: WatchedSystem): string; - /** Additional files and folders to add */ - getAdditionalFileOrFolder?(): File[]; - /** initial list of files to emit if not the default list */ - firstReloadFileList?: string[]; - } - function getInitialState({ configObj = {}, firstCompilationEmitFiles, getEmitLine, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { - const host = createWatchedSystem([]); - const getOutputName = getEmitLine ? (file: File) => getEmitLine(file, host) : - (file: File) => getEmittedLineForMultiFileOutput(file, host); - - const moduleFile1 = getFileOrFolderEmit({ - path: moduleFile1Path, - content: "export function Foo() { };", - }, getOutputName); - - const file1Consumer1 = getFileOrFolderEmit({ - path: file1Consumer1Path, - content: `import {Foo} from "./moduleFile1"; export var y = 10;`, - }, getOutputName); - - const file1Consumer2 = getFileOrFolderEmit({ - path: "/a/b/file1Consumer2.ts", - content: `import {Foo} from "./moduleFile1"; let z = 10;`, - }, getOutputName); - - const moduleFile2 = getFileOrFolderEmit({ - path: "/a/b/moduleFile2.ts", - content: `export var Foo4 = 10;`, - }, getOutputName); - - const globalFile3 = getFileOrFolderEmit({ - path: "/a/b/globalFile3.ts", - content: `interface GlobalFoo { age: number }` - }); - - const additionalFiles = getAdditionalFileOrFolder ? - map(getAdditionalFileOrFolder(), file => getFileOrFolderEmit(file, getOutputName)) : - []; - - (configObj.compilerOptions || (configObj.compilerOptions = {})).listEmittedFiles = true; - const configFile = getFileOrFolderEmit({ - path: configFilePath, - content: JSON.stringify(configObj) - }); - - const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile, ...additionalFiles]; - let allEmittedFiles = getEmittedLines(files); - host.reloadFS(firstReloadFileList ? getFiles(firstReloadFileList) : files); - - // Initial compile - createWatchOfConfigFile(configFile.path, host); - if (firstCompilationEmitFiles) { - checkAffectedLines(host, getFiles(firstCompilationEmitFiles), allEmittedFiles); - } - else { - checkOutputContains(host, allEmittedFiles); - } - host.clearOutput(); - - return { - moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, - files, - getFile, - verifyAffectedFiles, - verifyAffectedAllFiles, - getOutputName - }; - - function getFiles(filelist: string[]) { - return map(filelist, getFile); - } - - function getFile(fileName: string) { - return find(files, file => file.path === fileName)!; - } - - function verifyAffectedAllFiles() { - host.reloadFS(files); - host.checkTimeoutQueueLengthAndRun(1); - checkOutputContains(host, allEmittedFiles); - host.clearOutput(); - } - - function verifyAffectedFiles(expected: FileOrFolderEmit[], filesToReload?: FileOrFolderEmit[]) { - if (!filesToReload) { - filesToReload = files; - } - else if (filesToReload.length > files.length) { - allEmittedFiles = getEmittedLines(filesToReload); - } - host.reloadFS(filesToReload); - host.checkTimeoutQueueLengthAndRun(1); - checkAffectedLines(host, expected, allEmittedFiles); - host.clearOutput(); - } - } - - it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { - const { - moduleFile1, file1Consumer1, file1Consumer2, - verifyAffectedFiles - } = getInitialState(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` - moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; - verifyAffectedFiles([moduleFile1]); - }); - - it("should be up-to-date with the reference map changes", () => { - const { - moduleFile1, file1Consumer1, file1Consumer2, - verifyAffectedFiles - } = getInitialState(); - - // Change file1Consumer1 content to `export let y = Foo();` - file1Consumer1.content = `export let y = Foo();`; - verifyAffectedFiles([file1Consumer1]); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer2]); - - // Add the import statements back to file1Consumer1 - file1Consumer1.content = `import {Foo} from "./moduleFile1";let y = Foo();`; - verifyAffectedFiles([file1Consumer1]); - - // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` - moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer2, file1Consumer1]); - - // Multiple file edits in one go: - - // Change file1Consumer1 content to `export let y = Foo();` - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - file1Consumer1.content = `export let y = Foo();`; - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); - }); - - it("should be up-to-date with deleted files", () => { - const { - moduleFile1, file1Consumer1, file1Consumer2, - files, - verifyAffectedFiles - } = getInitialState(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - - // Delete file1Consumer2 - const filesToLoad = mapDefined(files, file => file === file1Consumer2 ? undefined : file); - verifyAffectedFiles([moduleFile1, file1Consumer1], filesToLoad); - }); - - it("should be up-to-date with newly created files", () => { - const { - moduleFile1, file1Consumer1, file1Consumer2, - files, - verifyAffectedFiles, - getOutputName - } = getInitialState(); - - const file1Consumer3 = getFileOrFolderEmit({ - path: "/a/b/file1Consumer3.ts", - content: `import {Foo} from "./moduleFile1"; let y = Foo();` - }, getOutputName); - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer3, file1Consumer2], files.concat(file1Consumer3)); - }); - - it("should detect changes in non-root files", () => { - const { - moduleFile1, file1Consumer1, - verifyAffectedFiles - } = getInitialState({ configObj: { files: [file1Consumer1Path] }, firstCompilationEmitFiles: [file1Consumer1Path, moduleFile1Path] }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1]); - - // change file1 internal, and verify only file1 is affected - moduleFile1.content += "var T1: number;"; - verifyAffectedFiles([moduleFile1]); - }); - - it("should return all files if a global file changed shape", () => { - const { - globalFile3, verifyAffectedAllFiles - } = getInitialState(); - - globalFile3.content += "var T2: string;"; - verifyAffectedAllFiles(); - }); - - it("should always return the file itself if '--isolatedModules' is specified", () => { - const { - moduleFile1, verifyAffectedFiles - } = getInitialState({ configObj: { compilerOptions: { isolatedModules: true } } }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1]); - }); - - it("should always return the file itself if '--out' or '--outFile' is specified", () => { - const outFilePath = "/a/b/out.js"; - const { - moduleFile1, verifyAffectedFiles - } = getInitialState({ - configObj: { compilerOptions: { module: "system", outFile: outFilePath } }, - getEmitLine: (_, host) => getEmittedLineForSingleFileOutput(outFilePath, host) - }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1]); - }); - - it("should return cascaded affected file list", () => { - const file1Consumer1Consumer1: File = { - path: "/a/b/file1Consumer1Consumer1.ts", - content: `import {y} from "./file1Consumer1";` - }; - const { - moduleFile1, file1Consumer1, file1Consumer2, verifyAffectedFiles, getFile - } = getInitialState({ - getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] - }); - - const file1Consumer1Consumer1Emit = getFile(file1Consumer1Consumer1.path); - file1Consumer1.content += "export var T: number;"; - verifyAffectedFiles([file1Consumer1, file1Consumer1Consumer1Emit]); - - // Doesnt change the shape of file1Consumer1 - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); - - // Change both files before the timeout - file1Consumer1.content += "export var T2: number;"; - moduleFile1.content = `export var T2: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer1Consumer1Emit]); - }); - - it("should work fine for files with circular references", () => { - // TODO: do not exit on such errors? Just continue to watch the files for update in watch mode - - const file1: File = { - path: "/a/b/file1.ts", - content: ` - /// - export var t1 = 10;` - }; - const file2: File = { - path: "/a/b/file2.ts", - content: ` - /// - export var t2 = 10;` - }; - const { - configFile, - getFile, - verifyAffectedFiles - } = getInitialState({ - firstCompilationEmitFiles: [file1.path, file2.path], - getAdditionalFileOrFolder: () => [file1, file2], - firstReloadFileList: [libFile.path, file1.path, file2.path, configFilePath] - }); - const file1Emit = getFile(file1.path), file2Emit = getFile(file2.path); - - file1Emit.content += "export var t3 = 10;"; - verifyAffectedFiles([file1Emit, file2Emit], [file1, file2, libFile, configFile]); - - }); - - it("should detect removed code file", () => { - const referenceFile1: File = { - path: "/a/b/referenceFile1.ts", - content: ` - /// - export var x = Foo();` - }; - const { - configFile, - getFile, - verifyAffectedFiles - } = getInitialState({ - firstCompilationEmitFiles: [referenceFile1.path, moduleFile1Path], - getAdditionalFileOrFolder: () => [referenceFile1], - firstReloadFileList: [libFile.path, referenceFile1.path, moduleFile1Path, configFilePath] - }); - - const referenceFile1Emit = getFile(referenceFile1.path); - verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]); - }); - - it("should detect non-existing code file", () => { - const referenceFile1: File = { - path: "/a/b/referenceFile1.ts", - content: ` - /// - export var x = Foo();` - }; - const { - configFile, - moduleFile2, - getFile, - verifyAffectedFiles - } = getInitialState({ - firstCompilationEmitFiles: [referenceFile1.path], - getAdditionalFileOrFolder: () => [referenceFile1], - firstReloadFileList: [libFile.path, referenceFile1.path, configFilePath] - }); - - const referenceFile1Emit = getFile(referenceFile1.path); - referenceFile1Emit.content += "export var yy = Foo();"; - verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]); - - // Create module File2 and see both files are saved - verifyAffectedFiles([referenceFile1Emit, moduleFile2], [libFile, moduleFile2, referenceFile1Emit, configFile]); - }); - }); - - describe("tsc-watch emit file content", () => { - interface EmittedFile extends File { - shouldBeWritten: boolean; - } - function getEmittedFiles(files: FileOrFolderEmit[], contents: string[]): EmittedFile[] { - return map(contents, (content, index) => { - return { - content, - path: changeExtension(files[index].path, Extension.Js), - shouldBeWritten: true - }; - } - ); - } - function verifyEmittedFiles(host: WatchedSystem, emittedFiles: EmittedFile[]) { - for (const { path, content, shouldBeWritten } of emittedFiles) { - if (shouldBeWritten) { - assert.isTrue(host.fileExists(path), `Expected file ${path} to be present`); - assert.equal(host.readFile(path), content, `Contents of file ${path} do not match`); - } - else { - assert.isNotTrue(host.fileExists(path), `Expected file ${path} to be absent`); - } - } - } - - function verifyEmittedFileContents(newLine: string, inputFiles: File[], initialEmittedFileContents: string[], - modifyFiles: (files: FileOrFolderEmit[], emitedFiles: EmittedFile[]) => FileOrFolderEmit[], configFile?: File) { - const host = createWatchedSystem([], { newLine }); - const files = concatenate( - map(inputFiles, file => getFileOrFolderEmit(file, fileToConvert => getEmittedLineForMultiFileOutput(fileToConvert, host))), - configFile ? [libFile, configFile] : [libFile] - ); - const allEmittedFiles = getEmittedLines(files); - host.reloadFS(files); - - // Initial compile - if (configFile) { - createWatchOfConfigFile(configFile.path, host); - } - else { - // First file as the root - createWatchOfFilesAndCompilerOptions([files[0].path], host, { listEmittedFiles: true }); - } - checkOutputContains(host, allEmittedFiles); - - const emittedFiles = getEmittedFiles(files, initialEmittedFileContents); - verifyEmittedFiles(host, emittedFiles); - host.clearOutput(); - - const affectedFiles = modifyFiles(files, emittedFiles); - host.reloadFS(files); - host.checkTimeoutQueueLengthAndRun(1); - checkAffectedLines(host, affectedFiles, allEmittedFiles); - - verifyEmittedFiles(host, emittedFiles); - } - - function verifyNewLine(newLine: string) { - const lines = ["var x = 1;", "var y = 2;"]; - const fileContent = lines.join(newLine); - const f = { - path: "/a/app.ts", - content: fileContent - }; - - verifyEmittedFileContents(newLine, [f], [fileContent + newLine], modifyFiles); - - function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { - files[0].content = fileContent + newLine + "var z = 3;"; - emittedFiles[0].content = files[0].content + newLine; - return [files[0]]; - } - } - - it("handles new lines: \\n", () => { - verifyNewLine("\n"); - }); - - it("handles new lines: \\r\\n", () => { - verifyNewLine("\r\n"); - }); - - it("should emit specified file", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export function Foo() { return 10; }` - }; - - const file2 = { - path: "/a/b/f2.ts", - content: `import {Foo} from "./f1"; export let y = Foo();` - }; - - const file3 = { - path: "/a/b/f3.ts", - content: `import {y} from "./f2"; let x = y;` - }; - - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { listEmittedFiles: true } }) - }; - - verifyEmittedFileContents("\r\n", [file1, file2, file3], [ - `"use strict";\r\nexports.__esModule = true;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`, - `"use strict";\r\nexports.__esModule = true;\r\nvar f1_1 = require("./f1");\r\nexports.y = f1_1.Foo();\r\n`, - `"use strict";\r\nexports.__esModule = true;\r\nvar f2_1 = require("./f2");\r\nvar x = f2_1.y;\r\n` - ], modifyFiles, configFile); - - function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { - files[0].content += `export function foo2() { return 2; }`; - emittedFiles[0].content += `function foo2() { return 2; }\r\nexports.foo2 = foo2;\r\n`; - emittedFiles[2].shouldBeWritten = false; - return files.slice(0, 2); - } - }); - - it("Elides const enums correctly in incremental compilation", () => { - const currentDirectory = "/user/someone/projects/myproject"; - const file1: File = { - path: `${currentDirectory}/file1.ts`, - content: "export const enum E1 { V = 1 }" - }; - const file2: File = { - path: `${currentDirectory}/file2.ts`, - content: `import { E1 } from "./file1"; export const enum E2 { V = E1.V }` - }; - const file3: File = { - path: `${currentDirectory}/file3.ts`, - content: `import { E2 } from "./file2"; const v: E2 = E2.V;` - }; - const strictAndEsModule = `"use strict";\nexports.__esModule = true;\n`; - verifyEmittedFileContents("\n", [file3, file2, file1], [ - `${strictAndEsModule}var v = 1 /* V */;\n`, - strictAndEsModule, - strictAndEsModule - ], modifyFiles); - - function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { - files[0].content += `function foo2() { return 2; }`; - emittedFiles[0].content += `function foo2() { return 2; }\n`; - emittedFiles[1].shouldBeWritten = false; - emittedFiles[2].shouldBeWritten = false; - return [files[0]]; - } - }); - - it("file is deleted and created as part of change", () => { - const projectLocation = "/home/username/project"; - const file: File = { - path: `${projectLocation}/app/file.ts`, - content: "var a = 10;" - }; - const fileJs = `${projectLocation}/app/file.js`; - const configFile: File = { - path: `${projectLocation}/tsconfig.json`, - content: JSON.stringify({ - include: [ - "app/**/*.ts" - ] - }) - }; - const files = [file, configFile, libFile]; - const host = createWatchedSystem(files, { currentDirectory: projectLocation, useCaseSensitiveFileNames: true }); - createWatchOfConfigFile("tsconfig.json", host); - verifyProgram(); - - file.content += "\nvar b = 10;"; - - host.reloadFS(files, { invokeFileDeleteCreateAsPartInsteadOfChange: true }); - host.runQueuedTimeoutCallbacks(); - verifyProgram(); - - function verifyProgram() { - assert.isTrue(host.fileExists(fileJs)); - assert.equal(host.readFile(fileJs), file.content + "\n"); - } - }); - }); - - describe("tsc-watch with when module emit is specified as node", () => { - it("when instead of filechanged recursive directory watcher is invoked", () => { - const configFile: File = { - 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: File = { - path: "/a/rootFolder/project/Scripts/TypeScript.ts", - content: "var z = 10;" - }; - const file2: File = { - path: "/a/rootFolder/project/Scripts/Javascript.js", - content: "var zz = 10;" - }; - const files = [configFile, file1, file2, libFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfConfigFile(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); - }); - }); - describe("tsc-watch console clearing", () => { const currentDirectoryLog = "Current directory: / CaseSensitiveFileNames: false\n"; const fileWatcherAddedLog = [ From 53a6968f06a1944a6519a378c06b8c30ae015bd6 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 12:14:10 -0800 Subject: [PATCH 05/20] More refactoring for compile on save --- src/testRunner/unittests/compileOnSave.ts | 49 +++++++++++++++++++ .../unittests/tsserverProjectSystem.ts | 48 ------------------ 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/src/testRunner/unittests/compileOnSave.ts b/src/testRunner/unittests/compileOnSave.ts index 07a0aa5d3ae..2352ac58c19 100644 --- a/src/testRunner/unittests/compileOnSave.ts +++ b/src/testRunner/unittests/compileOnSave.ts @@ -502,6 +502,54 @@ namespace ts.projectSystem { ]); }); }); + + describe("tsserverProjectSystem emit with outFile or out setting", () => { + function test(opts: CompilerOptions, expectedUsesOutFile: boolean) { + const f1 = { + path: "/a/a.ts", + content: "let x = 1" + }; + const f2 = { + path: "/a/b.ts", + content: "let y = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: opts, + compileOnSave: true + }) + }; + const host = createServerHost([f1, f2, config]); + const session = projectSystem.createSession(host); + session.executeCommand({ + seq: 1, + type: "request", + command: "open", + arguments: { file: f1.path } + }); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); + const { response } = session.executeCommand({ + seq: 2, + type: "request", + command: "compileOnSaveAffectedFileList", + arguments: { file: f1.path } + }); + assert.equal((response).length, 1, "expected output for 1 project"); + assert.equal((response)[0].fileNames.length, 2, "expected output for 1 project"); + assert.equal((response)[0].projectUsesOutFile, expectedUsesOutFile, "usesOutFile"); + } + + it("projectUsesOutFile should not be returned if not set", () => { + test({}, /*expectedUsesOutFile*/ false); + }); + it("projectUsesOutFile should be true if outFile is set", () => { + test({ outFile: "/a/out.js" }, /*expectedUsesOutFile*/ true); + }); + it("projectUsesOutFile should be true if out is set", () => { + test({ out: "/a/out.js" }, /*expectedUsesOutFile*/ true); + }); + }); }); describe("compileOnSave:: EmitFile test", () => { @@ -646,4 +694,5 @@ namespace ts.projectSystem { } }); }); + } diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 3b7c276cfc4..122d9459e75 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -5607,54 +5607,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem emit with outFile or out setting", () => { - function test(opts: CompilerOptions, expectedUsesOutFile: boolean) { - const f1 = { - path: "/a/a.ts", - content: "let x = 1" - }; - const f2 = { - path: "/a/b.ts", - content: "let y = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: opts, - compileOnSave: true - }) - }; - const host = createServerHost([f1, f2, config]); - const session = createSession(host); - session.executeCommand({ - seq: 1, - type: "request", - command: "open", - arguments: { file: f1.path } - }); - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); - const { response } = session.executeCommand({ - seq: 2, - type: "request", - command: "compileOnSaveAffectedFileList", - arguments: { file: f1.path } - }); - assert.equal((response).length, 1, "expected output for 1 project"); - assert.equal((response)[0].fileNames.length, 2, "expected output for 1 project"); - assert.equal((response)[0].projectUsesOutFile, expectedUsesOutFile, "usesOutFile"); - } - - it("projectUsesOutFile should not be returned if not set", () => { - test({}, /*expectedUsesOutFile*/ false); - }); - it("projectUsesOutFile should be true if outFile is set", () => { - test({ outFile: "/a/out.js" }, /*expectedUsesOutFile*/ true); - }); - it("projectUsesOutFile should be true if out is set", () => { - test({ out: "/a/out.js" }, /*expectedUsesOutFile*/ true); - }); - }); - describe("tsserverProjectSystem import helpers", () => { it("should not crash in tsserver", () => { const f1 = { From 1b6db32ecd07121063d9ee4eafa2a1795c7dd070 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 12:22:14 -0800 Subject: [PATCH 06/20] Move error tests from tsserver project system to projectErrors --- src/testRunner/unittests/projectErrors.ts | 281 ++++++++++++++++ src/testRunner/unittests/tsserverHelpers.ts | 44 +-- .../unittests/tsserverProjectSystem.ts | 310 ------------------ 3 files changed, 303 insertions(+), 332 deletions(-) diff --git a/src/testRunner/unittests/projectErrors.ts b/src/testRunner/unittests/projectErrors.ts index d243e9d5ea4..6eb364b569d 100644 --- a/src/testRunner/unittests/projectErrors.ts +++ b/src/testRunner/unittests/projectErrors.ts @@ -199,4 +199,285 @@ namespace ts.projectSystem { } }); }); + + describe("tsserver:: Project Errors are reported as appropriate", () => { + function createErrorLogger() { + let hasError = false; + const errorLogger: server.Logger = { + close: noop, + hasLevel: () => true, + loggingEnabled: () => true, + perftrc: noop, + info: noop, + msg: (_s, type) => { + if (type === server.Msg.Err) { + hasError = true; + } + }, + startGroup: noop, + endGroup: noop, + getLogFileName: () => undefined + }; + return { + errorLogger, + hasError: () => hasError + }; + } + + it("document is not contained in project", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const corruptedConfig = { + path: "/a/b/tsconfig.json", + content: "{" + }; + const host = createServerHost([file1, corruptedConfig]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + + const project = projectService.findProject(corruptedConfig.path)!; + checkProjectRootFiles(project, [file1.path]); + }); + + describe("when opening new file that doesnt exist on disk yet", () => { + function verifyNonExistentFile(useProjectRoot: boolean) { + const folderPath = "/user/someuser/projects/someFolder"; + const fileInRoot: File = { + path: `/src/somefile.d.ts`, + content: "class c { }" + }; + const fileInProjectRoot: File = { + path: `${folderPath}/src/somefile.d.ts`, + content: "class c { }" + }; + const host = createServerHost([libFile, fileInRoot, fileInProjectRoot]); + const { hasError, errorLogger } = createErrorLogger(); + const session = createSession(host, { canUseEvents: true, logger: errorLogger, useInferredProjectPerProjectRoot: true }); + + const projectService = session.getProjectService(); + const untitledFile = "untitled:Untitled-1"; + const refPathNotFound1 = "../../../../../../typings/@epic/Core.d.ts"; + const refPathNotFound2 = "./src/somefile.d.ts"; + const fileContent = `/// +/// `; + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { + file: untitledFile, + fileContent, + scriptKindName: "TS", + projectRootPath: useProjectRoot ? folderPath : undefined + } + }); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const infoForUntitledAtProjectRoot = projectService.getScriptInfoForPath(`${folderPath.toLowerCase()}/${untitledFile.toLowerCase()}` as Path); + const infoForUnitiledAtRoot = projectService.getScriptInfoForPath(`/${untitledFile.toLowerCase()}` as Path); + const infoForSomefileAtProjectRoot = projectService.getScriptInfoForPath(`/${folderPath.toLowerCase()}/src/somefile.d.ts` as Path); + const infoForSomefileAtRoot = projectService.getScriptInfoForPath(`${fileInRoot.path.toLowerCase()}` as Path); + if (useProjectRoot) { + assert.isDefined(infoForUntitledAtProjectRoot); + assert.isUndefined(infoForUnitiledAtRoot); + } + else { + assert.isDefined(infoForUnitiledAtRoot); + assert.isUndefined(infoForUntitledAtProjectRoot); + } + assert.isUndefined(infoForSomefileAtRoot); + assert.isUndefined(infoForSomefileAtProjectRoot); + + // Since this is not js project so no typings are queued + host.checkTimeoutQueueLength(0); + + const newTimeoutId = host.getNextTimeoutId(); + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [untitledFile] + } + }); + host.checkTimeoutQueueLength(1); + + // Run the last one = get error request + host.runQueuedTimeoutCallbacks(newTimeoutId); + + assert.isFalse(hasError()); + host.checkTimeoutQueueLength(0); + checkErrorMessage(session, "syntaxDiag", { file: untitledFile, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(); + assert.isFalse(hasError()); + const errorOffset = fileContent.indexOf(refPathNotFound1) + 1; + checkErrorMessage(session, "semanticDiag", { + file: untitledFile, + diagnostics: [ + createDiagnostic({ line: 1, offset: errorOffset }, { line: 1, offset: errorOffset + refPathNotFound1.length }, Diagnostics.File_0_not_found, [refPathNotFound1], "error"), + createDiagnostic({ line: 2, offset: errorOffset }, { line: 2, offset: errorOffset + refPathNotFound2.length }, Diagnostics.File_0_not_found, [refPathNotFound2.substr(2)], "error") + ] + }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + assert.isFalse(hasError()); + checkErrorMessage(session, "suggestionDiag", { file: untitledFile, diagnostics: [] }); + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); + } + + it("has projectRoot", () => { + verifyNonExistentFile(/*useProjectRoot*/ true); + }); + + it("does not have projectRoot", () => { + verifyNonExistentFile(/*useProjectRoot*/ false); + }); + }); + + it("folder rename updates project structure and reports no errors", () => { + const projectDir = "/a/b/projects/myproject"; + const app: File = { + path: `${projectDir}/bar/app.ts`, + content: "class Bar implements foo.Foo { getFoo() { return ''; } get2() { return 1; } }" + }; + const foo: File = { + path: `${projectDir}/foo/foo.ts`, + content: "declare namespace foo { interface Foo { get2(): number; getFoo(): string; } }" + }; + const configFile: File = { + path: `${projectDir}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { module: "none", targer: "es5" }, exclude: ["node_modules"] }) + }; + const host = createServerHost([app, foo, configFile]); + const session = createSession(host, { canUseEvents: true, }); + const projectService = session.getProjectService(); + + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { file: app.path, } + }); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.isDefined(projectService.configuredProjects.get(configFile.path)); + verifyErrorsInApp(); + + host.renameFolder(`${projectDir}/foo`, `${projectDir}/foo2`); + host.runQueuedTimeoutCallbacks(); + host.runQueuedTimeoutCallbacks(); + verifyErrorsInApp(); + + function verifyErrorsInApp() { + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [app.path] + } + }); + host.checkTimeoutQueueLengthAndRun(1); + checkErrorMessage(session, "syntaxDiag", { file: app.path, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(); + checkErrorMessage(session, "semanticDiag", { file: app.path, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + checkErrorMessage(session, "suggestionDiag", { file: app.path, diagnostics: [] }); + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); + } + }); + + it("Getting errors before opening file", () => { + const file: File = { + path: "/a/b/project/file.ts", + content: "let x: number = false;" + }; + const host = createServerHost([file, libFile]); + const { hasError, errorLogger } = createErrorLogger(); + const session = createSession(host, { canUseEvents: true, logger: errorLogger }); + + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [file.path] + } + }); + + host.runQueuedImmediateCallbacks(); + assert.isFalse(hasError()); + checkCompleteEvent(session, 1, expectedSequenceId); + session.clearMessages(); + }); + + it("Reports errors correctly when file referenced by inferred project root, is opened right after closing the root file", () => { + const projectRoot = "/user/username/projects/myproject"; + const app: File = { + path: `${projectRoot}/src/client/app.js`, + content: "" + }; + const serverUtilities: File = { + path: `${projectRoot}/src/server/utilities.js`, + content: `function getHostName() { return "hello"; } export { getHostName };` + }; + const backendTest: File = { + path: `${projectRoot}/test/backend/index.js`, + content: `import { getHostName } from '../../src/server/utilities';export default getHostName;` + }; + const files = [libFile, app, serverUtilities, backendTest]; + const host = createServerHost(files); + const session = createSession(host, { useInferredProjectPerProjectRoot: true, canUseEvents: true }); + openFilesForSession([{ file: app, projectRootPath: projectRoot }], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, { inferredProjects: 1 }); + const project = service.inferredProjects[0]; + checkProjectActualFiles(project, [libFile.path, app.path]); + openFilesForSession([{ file: backendTest, projectRootPath: projectRoot }], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkProjectActualFiles(project, files.map(f => f.path)); + checkErrors([backendTest.path, app.path]); + closeFilesForSession([backendTest], session); + openFilesForSession([{ file: serverUtilities.path, projectRootPath: projectRoot }], session); + checkErrors([serverUtilities.path, app.path]); + + function checkErrors(openFiles: [string, string]) { + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: protocol.CommandTypes.Geterr, + arguments: { + delay: 0, + files: openFiles + } + }); + + for (const openFile of openFiles) { + session.clearMessages(); + host.checkTimeoutQueueLength(3); + host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1); + + checkErrorMessage(session, "syntaxDiag", { file: openFile, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(); + checkErrorMessage(session, "semanticDiag", { file: openFile, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + checkErrorMessage(session, "suggestionDiag", { file: openFile, diagnostics: [] }); + } + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); + } + }); + }); } diff --git a/src/testRunner/unittests/tsserverHelpers.ts b/src/testRunner/unittests/tsserverHelpers.ts index 5162f17d6ae..f5113b768fc 100644 --- a/src/testRunner/unittests/tsserverHelpers.ts +++ b/src/testRunner/unittests/tsserverHelpers.ts @@ -430,9 +430,9 @@ namespace ts.projectSystem { checkArray(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); } - //function checkProjectRootFiles(project: server.Project, expectedFiles: ReadonlyArray) { - // checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); - //} + export function checkProjectRootFiles(project: server.Project, expectedFiles: ReadonlyArray) { + checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); + } export function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { dir = normalizePath(dir); @@ -613,17 +613,17 @@ namespace ts.projectSystem { // verifyDiagnostics(actual, []); //} - //function checkErrorMessage(session: TestSession, eventName: protocol.DiagnosticEventKind, diagnostics: protocol.DiagnosticEventBody, isMostRecent = false): void { - // checkNthEvent(session, server.toEvent(eventName, diagnostics), 0, isMostRecent); - //} + export function checkErrorMessage(session: TestSession, eventName: protocol.DiagnosticEventKind, diagnostics: protocol.DiagnosticEventBody, isMostRecent = false): void { + checkNthEvent(session, server.toEvent(eventName, diagnostics), 0, isMostRecent); + } - //function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: ReadonlyArray = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}, relatedInformation?: protocol.DiagnosticRelatedInformation[]): protocol.Diagnostic { - // return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, relatedInformation, source: undefined }; - //} + export function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: ReadonlyArray = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}, relatedInformation?: protocol.DiagnosticRelatedInformation[]): protocol.Diagnostic { + return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, relatedInformation, source: undefined }; + } - //function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number, isMostRecent = true): void { - // checkNthEvent(session, server.toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, isMostRecent); - //} + export function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number, isMostRecent = true): void { + checkNthEvent(session, server.toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, isMostRecent); + } //function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) { // checkNthEvent(session, server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); @@ -635,18 +635,18 @@ namespace ts.projectSystem { // } //} - //function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { - // const events = session.events; - // assert.deepEqual(events[index], expectedEvent, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`); + export function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { + const events = session.events; + assert.deepEqual(events[index], expectedEvent, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`); - // const outputs = session.host.getOutput(); - // assert.equal(outputs[index], server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, session.host.newLine)); + const outputs = session.host.getOutput(); + assert.equal(outputs[index], server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, session.host.newLine)); - // if (isMostRecent) { - // assert.strictEqual(events.length, index + 1, JSON.stringify(events)); - // assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs)); - // } - //} + if (isMostRecent) { + assert.strictEqual(events.length, index + 1, JSON.stringify(events)); + assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs)); + } + } //function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem { // return { diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 122d9459e75..3ccb7b2ffa0 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -41,10 +41,6 @@ namespace ts.projectSystem { } } - function checkProjectRootFiles(project: server.Project, expectedFiles: ReadonlyArray) { - checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); - } - function getNodeModuleDirectories(dir: string) { return getRootsToWatchWithAncestorDirectory(dir, nodeModules); } @@ -134,18 +130,6 @@ namespace ts.projectSystem { verifyDiagnostics(actual, []); } - function checkErrorMessage(session: TestSession, eventName: protocol.DiagnosticEventKind, diagnostics: protocol.DiagnosticEventBody, isMostRecent = false): void { - checkNthEvent(session, server.toEvent(eventName, diagnostics), 0, isMostRecent); - } - - function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: ReadonlyArray = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}, relatedInformation?: protocol.DiagnosticRelatedInformation[]): protocol.Diagnostic { - return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, relatedInformation, source: undefined }; - } - - function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number, isMostRecent = true): void { - checkNthEvent(session, server.toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, isMostRecent); - } - function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) { checkNthEvent(session, server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); } @@ -156,19 +140,6 @@ namespace ts.projectSystem { } } - function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { - const events = session.events; - assert.deepEqual(events[index], expectedEvent, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`); - - const outputs = session.host.getOutput(); - assert.equal(outputs[index], server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, session.host.newLine)); - - if (isMostRecent) { - assert.strictEqual(events.length, index + 1, JSON.stringify(events)); - assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs)); - } - } - describe("tsserverProjectSystem general functionality", () => { const commonFile1: File = { path: "/a/b/commonFile1.ts", @@ -3377,287 +3348,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem Proper errors", () => { - function createErrorLogger() { - let hasError = false; - const errorLogger: server.Logger = { - close: noop, - hasLevel: () => true, - loggingEnabled: () => true, - perftrc: noop, - info: noop, - msg: (_s, type) => { - if (type === server.Msg.Err) { - hasError = true; - } - }, - startGroup: noop, - endGroup: noop, - getLogFileName: () => undefined - }; - return { - errorLogger, - hasError: () => hasError - }; - } - - it("document is not contained in project", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const corruptedConfig = { - path: "/a/b/tsconfig.json", - content: "{" - }; - const host = createServerHost([file1, corruptedConfig]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - - const project = projectService.findProject(corruptedConfig.path)!; - checkProjectRootFiles(project, [file1.path]); - }); - - describe("when opening new file that doesnt exist on disk yet", () => { - function verifyNonExistentFile(useProjectRoot: boolean) { - const folderPath = "/user/someuser/projects/someFolder"; - const fileInRoot: File = { - path: `/src/somefile.d.ts`, - content: "class c { }" - }; - const fileInProjectRoot: File = { - path: `${folderPath}/src/somefile.d.ts`, - content: "class c { }" - }; - const host = createServerHost([libFile, fileInRoot, fileInProjectRoot]); - const { hasError, errorLogger } = createErrorLogger(); - const session = createSession(host, { canUseEvents: true, logger: errorLogger, useInferredProjectPerProjectRoot: true }); - - const projectService = session.getProjectService(); - const untitledFile = "untitled:Untitled-1"; - const refPathNotFound1 = "../../../../../../typings/@epic/Core.d.ts"; - const refPathNotFound2 = "./src/somefile.d.ts"; - const fileContent = `/// -/// `; - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { - file: untitledFile, - fileContent, - scriptKindName: "TS", - projectRootPath: useProjectRoot ? folderPath : undefined - } - }); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const infoForUntitledAtProjectRoot = projectService.getScriptInfoForPath(`${folderPath.toLowerCase()}/${untitledFile.toLowerCase()}` as Path); - const infoForUnitiledAtRoot = projectService.getScriptInfoForPath(`/${untitledFile.toLowerCase()}` as Path); - const infoForSomefileAtProjectRoot = projectService.getScriptInfoForPath(`/${folderPath.toLowerCase()}/src/somefile.d.ts` as Path); - const infoForSomefileAtRoot = projectService.getScriptInfoForPath(`${fileInRoot.path.toLowerCase()}` as Path); - if (useProjectRoot) { - assert.isDefined(infoForUntitledAtProjectRoot); - assert.isUndefined(infoForUnitiledAtRoot); - } - else { - assert.isDefined(infoForUnitiledAtRoot); - assert.isUndefined(infoForUntitledAtProjectRoot); - } - assert.isUndefined(infoForSomefileAtRoot); - assert.isUndefined(infoForSomefileAtProjectRoot); - - // Since this is not js project so no typings are queued - host.checkTimeoutQueueLength(0); - - const newTimeoutId = host.getNextTimeoutId(); - const expectedSequenceId = session.getNextSeq(); - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [untitledFile] - } - }); - host.checkTimeoutQueueLength(1); - - // Run the last one = get error request - host.runQueuedTimeoutCallbacks(newTimeoutId); - - assert.isFalse(hasError()); - host.checkTimeoutQueueLength(0); - checkErrorMessage(session, "syntaxDiag", { file: untitledFile, diagnostics: [] }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(); - assert.isFalse(hasError()); - const errorOffset = fileContent.indexOf(refPathNotFound1) + 1; - checkErrorMessage(session, "semanticDiag", { - file: untitledFile, - diagnostics: [ - createDiagnostic({ line: 1, offset: errorOffset }, { line: 1, offset: errorOffset + refPathNotFound1.length }, Diagnostics.File_0_not_found, [refPathNotFound1], "error"), - createDiagnostic({ line: 2, offset: errorOffset }, { line: 2, offset: errorOffset + refPathNotFound2.length }, Diagnostics.File_0_not_found, [refPathNotFound2.substr(2)], "error") - ] - }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - assert.isFalse(hasError()); - checkErrorMessage(session, "suggestionDiag", { file: untitledFile, diagnostics: [] }); - checkCompleteEvent(session, 2, expectedSequenceId); - session.clearMessages(); - } - - it("has projectRoot", () => { - verifyNonExistentFile(/*useProjectRoot*/ true); - }); - - it("does not have projectRoot", () => { - verifyNonExistentFile(/*useProjectRoot*/ false); - }); - }); - - it("folder rename updates project structure and reports no errors", () => { - const projectDir = "/a/b/projects/myproject"; - const app: File = { - path: `${projectDir}/bar/app.ts`, - content: "class Bar implements foo.Foo { getFoo() { return ''; } get2() { return 1; } }" - }; - const foo: File = { - path: `${projectDir}/foo/foo.ts`, - content: "declare namespace foo { interface Foo { get2(): number; getFoo(): string; } }" - }; - const configFile: File = { - path: `${projectDir}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { module: "none", targer: "es5" }, exclude: ["node_modules"] }) - }; - const host = createServerHost([app, foo, configFile]); - const session = createSession(host, { canUseEvents: true, }); - const projectService = session.getProjectService(); - - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { file: app.path, } - }); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.isDefined(projectService.configuredProjects.get(configFile.path)); - verifyErrorsInApp(); - - host.renameFolder(`${projectDir}/foo`, `${projectDir}/foo2`); - host.runQueuedTimeoutCallbacks(); - host.runQueuedTimeoutCallbacks(); - verifyErrorsInApp(); - - function verifyErrorsInApp() { - session.clearMessages(); - const expectedSequenceId = session.getNextSeq(); - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [app.path] - } - }); - host.checkTimeoutQueueLengthAndRun(1); - checkErrorMessage(session, "syntaxDiag", { file: app.path, diagnostics: [] }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(); - checkErrorMessage(session, "semanticDiag", { file: app.path, diagnostics: [] }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - checkErrorMessage(session, "suggestionDiag", { file: app.path, diagnostics: [] }); - checkCompleteEvent(session, 2, expectedSequenceId); - session.clearMessages(); - } - }); - - it("Getting errors before opening file", () => { - const file: File = { - path: "/a/b/project/file.ts", - content: "let x: number = false;" - }; - const host = createServerHost([file, libFile]); - const { hasError, errorLogger } = createErrorLogger(); - const session = createSession(host, { canUseEvents: true, logger: errorLogger }); - - session.clearMessages(); - const expectedSequenceId = session.getNextSeq(); - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [file.path] - } - }); - - host.runQueuedImmediateCallbacks(); - assert.isFalse(hasError()); - checkCompleteEvent(session, 1, expectedSequenceId); - session.clearMessages(); - }); - - it("Reports errors correctly when file referenced by inferred project root, is opened right after closing the root file", () => { - const projectRoot = "/user/username/projects/myproject"; - const app: File = { - path: `${projectRoot}/src/client/app.js`, - content: "" - }; - const serverUtilities: File = { - path: `${projectRoot}/src/server/utilities.js`, - content: `function getHostName() { return "hello"; } export { getHostName };` - }; - const backendTest: File = { - path: `${projectRoot}/test/backend/index.js`, - content: `import { getHostName } from '../../src/server/utilities';export default getHostName;` - }; - const files = [libFile, app, serverUtilities, backendTest]; - const host = createServerHost(files); - const session = createSession(host, { useInferredProjectPerProjectRoot: true, canUseEvents: true }); - openFilesForSession([{ file: app, projectRootPath: projectRoot }], session); - const service = session.getProjectService(); - checkNumberOfProjects(service, { inferredProjects: 1 }); - const project = service.inferredProjects[0]; - checkProjectActualFiles(project, [libFile.path, app.path]); - openFilesForSession([{ file: backendTest, projectRootPath: projectRoot }], session); - checkNumberOfProjects(service, { inferredProjects: 1 }); - checkProjectActualFiles(project, files.map(f => f.path)); - checkErrors([backendTest.path, app.path]); - closeFilesForSession([backendTest], session); - openFilesForSession([{ file: serverUtilities.path, projectRootPath: projectRoot }], session); - checkErrors([serverUtilities.path, app.path]); - - function checkErrors(openFiles: [string, string]) { - const expectedSequenceId = session.getNextSeq(); - session.executeCommandSeq({ - command: protocol.CommandTypes.Geterr, - arguments: { - delay: 0, - files: openFiles - } - }); - - for (const openFile of openFiles) { - session.clearMessages(); - host.checkTimeoutQueueLength(3); - host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1); - - checkErrorMessage(session, "syntaxDiag", { file: openFile, diagnostics: [] }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(); - checkErrorMessage(session, "semanticDiag", { file: openFile, diagnostics: [] }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - checkErrorMessage(session, "suggestionDiag", { file: openFile, diagnostics: [] }); - } - checkCompleteEvent(session, 2, expectedSequenceId); - session.clearMessages(); - } - }); - }); - describe("tsserverProjectSystem autoDiscovery", () => { it("does not depend on extension", () => { const file1 = { From 37a080bca98d5ca724366c319b5f8503e27b652a Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 12:25:19 -0800 Subject: [PATCH 07/20] tsserver's typingInstaller test into typingsInstaller unittest --- .../unittests/tsserverProjectSystem.ts | 78 ------------------- src/testRunner/unittests/typingsInstaller.ts | 78 +++++++++++++++++++ 2 files changed, 78 insertions(+), 78 deletions(-) diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 3ccb7b2ffa0..69de01cf969 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -7329,84 +7329,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem typingsInstaller on inferred Project", () => { - it("when projectRootPath is provided", () => { - const projects = "/users/username/projects"; - const projectRootPath = `${projects}/san2`; - const file: File = { - path: `${projectRootPath}/x.js`, - content: "const aaaaaaav = 1;" - }; - - const currentDirectory = `${projects}/anotherProject`; - const packageJsonInCurrentDirectory: File = { - path: `${currentDirectory}/package.json`, - content: JSON.stringify({ - devDependencies: { - pkgcurrentdirectory: "" - }, - }) - }; - const packageJsonOfPkgcurrentdirectory: File = { - path: `${currentDirectory}/node_modules/pkgcurrentdirectory/package.json`, - content: JSON.stringify({ - name: "pkgcurrentdirectory", - main: "index.js", - typings: "index.d.ts" - }) - }; - const indexOfPkgcurrentdirectory: File = { - path: `${currentDirectory}/node_modules/pkgcurrentdirectory/index.d.ts`, - content: "export function foo() { }" - }; - - const typingsCache = `/users/username/Library/Caches/typescript/2.7`; - const typingsCachePackageJson: File = { - path: `${typingsCache}/package.json`, - content: JSON.stringify({ - devDependencies: { - }, - }) - }; - const typingsCachePackageLockJson: File = { - path: `${typingsCache}/package-lock.json`, - content: JSON.stringify({ - dependencies: { - }, - }) - }; - - const files = [file, packageJsonInCurrentDirectory, packageJsonOfPkgcurrentdirectory, indexOfPkgcurrentdirectory, typingsCachePackageJson, typingsCachePackageLockJson]; - const host = createServerHost(files, { currentDirectory }); - - const typesRegistry = createTypesRegistry("pkgcurrentdirectory"); - const typingsInstaller = new TestTypingsInstaller(typingsCache, /*throttleLimit*/ 5, host, typesRegistry); - - const projectService = createProjectService(host, { typingsInstaller }); - - projectService.setCompilerOptionsForInferredProjects({ - module: ModuleKind.CommonJS, - target: ScriptTarget.ES2016, - jsx: JsxEmit.Preserve, - experimentalDecorators: true, - allowJs: true, - allowSyntheticDefaultImports: true, - allowNonTsExtensions: true - }); - - projectService.openClientFile(file.path, file.content, ScriptKind.JS, projectRootPath); - - const project = projectService.inferredProjects[0]; - assert.isDefined(project); - - // Ensure that we use result from types cache when getting ls - assert.isDefined(project.getLanguageService()); - - // Verify that the pkgcurrentdirectory from the current directory isnt picked up - checkProjectActualFiles(project, [file.path]); - }); - }); - describe("tsserverProjectSystem with symLinks", () => { it("rename in common file renames all project", () => { const projects = "/users/username/projects"; diff --git a/src/testRunner/unittests/typingsInstaller.ts b/src/testRunner/unittests/typingsInstaller.ts index 2b18af6a71f..f0cd9549e6c 100644 --- a/src/testRunner/unittests/typingsInstaller.ts +++ b/src/testRunner/unittests/typingsInstaller.ts @@ -1774,4 +1774,82 @@ namespace ts.projectSystem { `, ["foo"], [fooAA, fooAB, fooAC]); }); }); + + describe("typingsInstaller:: tsserver:: with inferred Project", () => { + it("when projectRootPath is provided", () => { + const projects = "/users/username/projects"; + const projectRootPath = `${projects}/san2`; + const file: File = { + path: `${projectRootPath}/x.js`, + content: "const aaaaaaav = 1;" + }; + + const currentDirectory = `${projects}/anotherProject`; + const packageJsonInCurrentDirectory: File = { + path: `${currentDirectory}/package.json`, + content: JSON.stringify({ + devDependencies: { + pkgcurrentdirectory: "" + }, + }) + }; + const packageJsonOfPkgcurrentdirectory: File = { + path: `${currentDirectory}/node_modules/pkgcurrentdirectory/package.json`, + content: JSON.stringify({ + name: "pkgcurrentdirectory", + main: "index.js", + typings: "index.d.ts" + }) + }; + const indexOfPkgcurrentdirectory: File = { + path: `${currentDirectory}/node_modules/pkgcurrentdirectory/index.d.ts`, + content: "export function foo() { }" + }; + + const typingsCache = `/users/username/Library/Caches/typescript/2.7`; + const typingsCachePackageJson: File = { + path: `${typingsCache}/package.json`, + content: JSON.stringify({ + devDependencies: { + }, + }) + }; + const typingsCachePackageLockJson: File = { + path: `${typingsCache}/package-lock.json`, + content: JSON.stringify({ + dependencies: { + }, + }) + }; + + const files = [file, packageJsonInCurrentDirectory, packageJsonOfPkgcurrentdirectory, indexOfPkgcurrentdirectory, typingsCachePackageJson, typingsCachePackageLockJson]; + const host = createServerHost(files, { currentDirectory }); + + const typesRegistry = createTypesRegistry("pkgcurrentdirectory"); + const typingsInstaller = new TestTypingsInstaller(typingsCache, /*throttleLimit*/ 5, host, typesRegistry); + + const projectService = createProjectService(host, { typingsInstaller }); + + projectService.setCompilerOptionsForInferredProjects({ + module: ModuleKind.CommonJS, + target: ScriptTarget.ES2016, + jsx: JsxEmit.Preserve, + experimentalDecorators: true, + allowJs: true, + allowSyntheticDefaultImports: true, + allowNonTsExtensions: true + }); + + projectService.openClientFile(file.path, file.content, ScriptKind.JS, projectRootPath); + + const project = projectService.inferredProjects[0]; + assert.isDefined(project); + + // Ensure that we use result from types cache when getting ls + assert.isDefined(project.getLanguageService()); + + // Verify that the pkgcurrentdirectory from the current directory isnt picked up + checkProjectActualFiles(project, [file.path]); + }); + }); } From 03d66dd2ef066b29e43c6ebce0cdf733e37cc5d4 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 12:27:23 -0800 Subject: [PATCH 08/20] Emit error tsserver tests into projectErrors --- src/testRunner/unittests/projectErrors.ts | 83 +++++++++++++++++++ .../unittests/tsserverProjectSystem.ts | 83 ------------------- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/src/testRunner/unittests/projectErrors.ts b/src/testRunner/unittests/projectErrors.ts index 6eb364b569d..15bbab2a551 100644 --- a/src/testRunner/unittests/projectErrors.ts +++ b/src/testRunner/unittests/projectErrors.ts @@ -480,4 +480,87 @@ namespace ts.projectSystem { } }); }); + + describe("tsserver:: Project Errors dont include overwrite emit error", () => { + it("for inferred project", () => { + const f1 = { + path: "/a/b/f1.js", + content: "function test1() { }" + }; + const host = createServerHost([f1, libFile]); + const session = createSession(host); + openFilesForSession([f1], session); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const projectName = projectService.inferredProjects[0].getProjectName(); + + const diags = session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName: projectName } + }).response as ReadonlyArray; + assert.isTrue(diags.length === 0); + + session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsForInferredProjects, + seq: 3, + arguments: { options: { module: ModuleKind.CommonJS } } + }); + const diagsAfterUpdate = session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 4, + arguments: { projectFileName: projectName } + }).response as ReadonlyArray; + assert.isTrue(diagsAfterUpdate.length === 0); + }); + + it("for external project", () => { + const f1 = { + path: "/a/b/f1.js", + content: "function test1() { }" + }; + const host = createServerHost([f1, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); + const projectFileName = "/a/b/project.csproj"; + const externalFiles = toExternalFiles([f1.path]); + projectService.openExternalProject({ + projectFileName, + rootFiles: externalFiles, + options: {} + }); + + checkNumberOfProjects(projectService, { externalProjects: 1 }); + + const diags = session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName } + }).response as ReadonlyArray; + assert.isTrue(diags.length === 0); + + session.executeCommand({ + type: "request", + command: server.CommandNames.OpenExternalProject, + seq: 3, + arguments: { + projectFileName, + rootFiles: externalFiles, + options: { module: ModuleKind.CommonJS } + } + }); + const diagsAfterUpdate = session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 4, + arguments: { projectFileName } + }).response as ReadonlyArray; + assert.isTrue(diagsAfterUpdate.length === 0); + }); + }); } diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 69de01cf969..214ddabdb85 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -5214,89 +5214,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem No overwrite emit error", () => { - it("for inferred project", () => { - const f1 = { - path: "/a/b/f1.js", - content: "function test1() { }" - }; - const host = createServerHost([f1, libFile]); - const session = createSession(host); - openFilesForSession([f1], session); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const projectName = projectService.inferredProjects[0].getProjectName(); - - const diags = session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName: projectName } - }).response as ReadonlyArray; - assert.isTrue(diags.length === 0); - - session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsForInferredProjects, - seq: 3, - arguments: { options: { module: ModuleKind.CommonJS } } - }); - const diagsAfterUpdate = session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 4, - arguments: { projectFileName: projectName } - }).response as ReadonlyArray; - assert.isTrue(diagsAfterUpdate.length === 0); - }); - - it("for external project", () => { - const f1 = { - path: "/a/b/f1.js", - content: "function test1() { }" - }; - const host = createServerHost([f1, libFile]); - const session = createSession(host); - const projectService = session.getProjectService(); - const projectFileName = "/a/b/project.csproj"; - const externalFiles = toExternalFiles([f1.path]); - projectService.openExternalProject({ - projectFileName, - rootFiles: externalFiles, - options: {} - }); - - checkNumberOfProjects(projectService, { externalProjects: 1 }); - - const diags = session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName } - }).response as ReadonlyArray; - assert.isTrue(diags.length === 0); - - session.executeCommand({ - type: "request", - command: server.CommandNames.OpenExternalProject, - seq: 3, - arguments: { - projectFileName, - rootFiles: externalFiles, - options: { module: ModuleKind.CommonJS } - } - }); - const diagsAfterUpdate = session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 4, - arguments: { projectFileName } - }).response as ReadonlyArray; - assert.isTrue(diagsAfterUpdate.length === 0); - }); - }); - describe("tsserverProjectSystem import helpers", () => { it("should not crash in tsserver", () => { const f1 = { From 39ec69e26e972757a626f61ac2fce156f7b6e4fa Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 12:34:16 -0800 Subject: [PATCH 09/20] Separate out tests for project loading events into their own unittest file --- src/testRunner/tsconfig.json | 1 + src/testRunner/unittests/tsserverHelpers.ts | 140 ++++----- .../unittests/tsserverProjectLoadingEvents.ts | 193 +++++++++++++ .../unittests/tsserverProjectSystem.ts | 268 ------------------ 4 files changed, 266 insertions(+), 336 deletions(-) create mode 100644 src/testRunner/unittests/tsserverProjectLoadingEvents.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index a95bc106fb1..3b8308af890 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -88,6 +88,7 @@ "unittests/tsconfigParsing.ts", "unittests/tscWatchEmit.ts", "unittests/tscWatchMode.ts", + "unittests/tsserverProjectLoadingEvents.ts", "unittests/tsserverProjectSystem.ts", "unittests/typingsInstaller.ts", "unittests/versionCache.ts", diff --git a/src/testRunner/unittests/tsserverHelpers.ts b/src/testRunner/unittests/tsserverHelpers.ts index f5113b768fc..18e6f5c6194 100644 --- a/src/testRunner/unittests/tsserverHelpers.ts +++ b/src/testRunner/unittests/tsserverHelpers.ts @@ -15,13 +15,13 @@ namespace ts.projectSystem { export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; - //const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; - //function mapOutputToJson(s: string) { - // return convertToObject( - // parseJsonText("json.json", s.replace(outputEventRegex, "")), - // [] - // ); - //} + const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; + export function mapOutputToJson(s: string) { + return convertToObject( + parseJsonText("json.json", s.replace(outputEventRegex, "")), + [] + ); + } export const customTypesMap = { path: "/typesMap.json", @@ -330,39 +330,39 @@ namespace ts.projectSystem { return new TestSession({ ...sessionOptions, ...opts }); } - //function createSessionWithEventTracking(host: server.ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) { - // const events: T[] = []; - // const session = createSession(host, { - // eventHandler: e => { - // if (e.eventName === eventName || eventNames.some(eventName => e.eventName === eventName)) { - // events.push(e as T); - // } - // } - // }); + export function createSessionWithEventTracking(host: server.ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) { + const events: T[] = []; + const session = createSession(host, { + eventHandler: e => { + if (e.eventName === eventName || eventNames.some(eventName => e.eventName === eventName)) { + events.push(e as T); + } + } + }); - // return { session, events }; - //} + return { session, events }; + } - //function createSessionWithDefaultEventHandler(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial = {}) { - // const session = createSession(host, { canUseEvents: true, ...opts }); + export function createSessionWithDefaultEventHandler(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial = {}) { + const session = createSession(host, { canUseEvents: true, ...opts }); - // return { - // session, - // getEvents, - // clearEvents - // }; + return { + session, + getEvents, + clearEvents + }; - // function getEvents() { - // return mapDefined(host.getOutput(), s => { - // const e = mapOutputToJson(s); - // return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined; - // }); - // } + function getEvents() { + return mapDefined(host.getOutput(), s => { + const e = mapOutputToJson(s); + return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined; + }); + } - // function clearEvents() { - // session.clearMessages(); - // } - //} + function clearEvents() { + session.clearMessages(); + } + } export interface CreateProjectServiceParameters { cancellationToken?: HostCancellationToken; @@ -467,23 +467,25 @@ namespace ts.projectSystem { // checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path)); //} - //function protocolLocationFromSubstring(str: string, substring: string): protocol.Location { - // const start = str.indexOf(substring); - // Debug.assert(start !== -1); - // return protocolToLocation(str)(start); - //} - //function protocolToLocation(text: string): (pos: number) => protocol.Location { - // const lineStarts = computeLineStarts(text); - // return pos => { - // const x = computeLineAndCharacterOfPosition(lineStarts, pos); - // return { line: x.line + 1, offset: x.character + 1 }; - // }; - //} - //function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan { - // const span = textSpanFromSubstring(str, substring, options); - // const toLocation = protocolToLocation(str); - // return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) }; - //} + export function protocolLocationFromSubstring(str: string, substring: string): protocol.Location { + const start = str.indexOf(substring); + Debug.assert(start !== -1); + return protocolToLocation(str)(start); + } + + function protocolToLocation(text: string): (pos: number) => protocol.Location { + const lineStarts = computeLineStarts(text); + return pos => { + const x = computeLineAndCharacterOfPosition(lineStarts, pos); + return { line: x.line + 1, offset: x.character + 1 }; + }; + } + + export function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan { + const span = textSpanFromSubstring(str, substring, options); + const toLocation = protocolToLocation(str); + return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) }; + } //function protocolRenameSpanFromSubstring( // str: string, // substring: string, @@ -492,11 +494,12 @@ namespace ts.projectSystem { //): protocol.RenameTextSpan { // return { ...protocolTextSpanFromSubstring(str, substring, options), ...prefixSuffixText }; //} - //function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan { - // const start = nthIndexOf(str, substring, options ? options.index : 0); - // Debug.assert(start !== -1); - // return createTextSpan(start, substring.length); - //} + + export function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan { + const start = nthIndexOf(str, substring, options ? options.index : 0); + Debug.assert(start !== -1); + return createTextSpan(start, substring.length); + } //function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs { // return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) }; //} @@ -509,18 +512,19 @@ namespace ts.projectSystem { //function renameLocation(file: File, substring: string, options?: SpanFromSubstringOptions): RenameLocation { // return documentSpanFromSubstring(file, substring, options); //} - //interface SpanFromSubstringOptions { - // readonly index: number; - //} - //function nthIndexOf(str: string, substr: string, n: number): number { - // let index = -1; - // for (; n >= 0; n--) { - // index = str.indexOf(substr, index + 1); - // if (index === -1) return -1; - // } - // return index; - //} + export interface SpanFromSubstringOptions { + readonly index: number; + } + + function nthIndexOf(str: string, substr: string, n: number): number { + let index = -1; + for (; n >= 0; n--) { + index = str.indexOf(substr, index + 1); + if (index === -1) return -1; + } + return index; + } /** * Test server cancellation token used to mock host token cancellation requests. diff --git a/src/testRunner/unittests/tsserverProjectLoadingEvents.ts b/src/testRunner/unittests/tsserverProjectLoadingEvents.ts new file mode 100644 index 00000000000..0ff256bd692 --- /dev/null +++ b/src/testRunner/unittests/tsserverProjectLoadingEvents.ts @@ -0,0 +1,193 @@ +namespace ts.projectSystem { + describe("tsserverProjectLoadingEvents:: ProjectLoadingStart and ProjectLoadingFinish events", () => { + const projectRoot = "/user/username/projects"; + const aTs: File = { + path: `${projectRoot}/a/a.ts`, + content: "export class A { }" + }; + const configA: File = { + path: `${projectRoot}/a/tsconfig.json`, + content: "{}" + }; + const bTsPath = `${projectRoot}/b/b.ts`; + const configBPath = `${projectRoot}/b/tsconfig.json`; + const files = [libFile, aTs, configA]; + + function verifyProjectLoadingStartAndFinish(createSession: (host: TestServerHost) => { + session: TestSession; + getNumberOfEvents: () => number; + clearEvents: () => void; + verifyProjectLoadEvents: (expected: [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]) => void; + }) { + function createSessionToVerifyEvent(files: ReadonlyArray) { + const host = createServerHost(files); + const originalReadFile = host.readFile; + const { session, getNumberOfEvents, clearEvents, verifyProjectLoadEvents } = createSession(host); + host.readFile = file => { + if (file === configA.path || file === configBPath) { + assert.equal(getNumberOfEvents(), 1, "Event for loading is sent before reading config file"); + } + return originalReadFile.call(host, file); + }; + const service = session.getProjectService(); + return { host, session, verifyEvent, verifyEventWithOpenTs, service, getNumberOfEvents }; + + function verifyEvent(project: server.Project, reason: string) { + verifyProjectLoadEvents([ + { eventName: server.ProjectLoadingStartEvent, data: { project, reason } }, + { eventName: server.ProjectLoadingFinishEvent, data: { project } } + ]); + clearEvents(); + } + + function verifyEventWithOpenTs(file: File, configPath: string, configuredProjects: number) { + openFilesForSession([file], session); + checkNumberOfProjects(service, { configuredProjects }); + const project = service.configuredProjects.get(configPath)!; + assert.isDefined(project); + verifyEvent(project, `Creating possible configured project for ${file.path} to open`); + } + } + + it("when project is created by open file", () => { + const bTs: File = { + path: bTsPath, + content: "export class B {}" + }; + const configB: File = { + path: configBPath, + content: "{}" + }; + const { verifyEventWithOpenTs } = createSessionToVerifyEvent(files.concat(bTs, configB)); + verifyEventWithOpenTs(aTs, configA.path, 1); + verifyEventWithOpenTs(bTs, configB.path, 2); + }); + + it("when change is detected in the config file", () => { + const { host, verifyEvent, verifyEventWithOpenTs, service } = createSessionToVerifyEvent(files); + verifyEventWithOpenTs(aTs, configA.path, 1); + + host.writeFile(configA.path, configA.content); + host.checkTimeoutQueueLengthAndRun(2); + const project = service.configuredProjects.get(configA.path)!; + verifyEvent(project, `Change in config file detected`); + }); + + it("when opening original location project", () => { + const aDTs: File = { + path: `${projectRoot}/a/a.d.ts`, + content: `export declare class A { +} +//# sourceMappingURL=a.d.ts.map +` + }; + const aDTsMap: File = { + path: `${projectRoot}/a/a.d.ts.map`, + content: `{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["./a.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;CAAI"}` + }; + const bTs: File = { + path: bTsPath, + content: `import {A} from "../a/a"; new A();` + }; + const configB: File = { + path: configBPath, + content: JSON.stringify({ + references: [{ path: "../a" }] + }) + }; + + const { service, session, verifyEventWithOpenTs, verifyEvent } = createSessionToVerifyEvent(files.concat(aDTs, aDTsMap, bTs, configB)); + verifyEventWithOpenTs(bTs, configB.path, 1); + + session.executeCommandSeq({ + command: protocol.CommandTypes.References, + arguments: { + file: bTs.path, + ...protocolLocationFromSubstring(bTs.content, "A()") + } + }); + + checkNumberOfProjects(service, { configuredProjects: 2 }); + const project = service.configuredProjects.get(configA.path)!; + assert.isDefined(project); + verifyEvent(project, `Creating project for original file: ${aTs.path} for location: ${aDTs.path}`); + }); + + describe("with external projects and config files ", () => { + const projectFileName = `${projectRoot}/a/project.csproj`; + + function createSession(lazyConfiguredProjectsFromExternalProject: boolean) { + const { session, service, verifyEvent: verifyEventWorker, getNumberOfEvents } = createSessionToVerifyEvent(files); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([aTs.path, configA.path]), + options: {} + }); + checkNumberOfProjects(service, { configuredProjects: 1 }); + return { session, service, verifyEvent, getNumberOfEvents }; + + function verifyEvent() { + const projectA = service.configuredProjects.get(configA.path)!; + assert.isDefined(projectA); + verifyEventWorker(projectA, `Creating configured project in external project: ${projectFileName}`); + } + } + + it("when lazyConfiguredProjectsFromExternalProject is false", () => { + const { verifyEvent } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ false); + verifyEvent(); + }); + + it("when lazyConfiguredProjectsFromExternalProject is true and file is opened", () => { + const { verifyEvent, getNumberOfEvents, session } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true); + assert.equal(getNumberOfEvents(), 0); + + openFilesForSession([aTs], session); + verifyEvent(); + }); + + it("when lazyConfiguredProjectsFromExternalProject is disabled", () => { + const { verifyEvent, getNumberOfEvents, service } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true); + assert.equal(getNumberOfEvents(), 0); + + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); + verifyEvent(); + }); + }); + } + + describe("when using event handler", () => { + verifyProjectLoadingStartAndFinish(host => { + const { session, events } = createSessionWithEventTracking(host, server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent); + return { + session, + getNumberOfEvents: () => events.length, + clearEvents: () => events.length = 0, + verifyProjectLoadEvents: expected => assert.deepEqual(events, expected) + }; + }); + }); + + describe("when using default event handler", () => { + verifyProjectLoadingStartAndFinish(host => { + const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler(host, [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]); + return { + session, + getNumberOfEvents: () => getEvents().length, + clearEvents, + verifyProjectLoadEvents + }; + + function verifyProjectLoadEvents(expected: [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]) { + const actual = getEvents().map(e => ({ eventName: e.event, data: e.body })); + const mappedExpected = expected.map(e => { + const { project, ...rest } = e.data; + return { eventName: e.eventName, data: { projectName: project.getProjectName(), ...rest } }; + }); + assert.deepEqual(actual, mappedExpected); + } + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 214ddabdb85..74522bcca8d 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -1,46 +1,4 @@ namespace ts.projectSystem { - const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; - function mapOutputToJson(s: string) { - return convertToObject( - parseJsonText("json.json", s.replace(outputEventRegex, "")), - [] - ); - } - - function createSessionWithEventTracking(host: server.ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) { - const events: T[] = []; - const session = createSession(host, { - eventHandler: e => { - if (e.eventName === eventName || eventNames.some(eventName => e.eventName === eventName)) { - events.push(e as T); - } - } - }); - - return { session, events }; - } - - function createSessionWithDefaultEventHandler(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial = {}) { - const session = createSession(host, { canUseEvents: true, ...opts }); - - return { - session, - getEvents, - clearEvents - }; - - function getEvents() { - return mapDefined(host.getOutput(), s => { - const e = mapOutputToJson(s); - return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined; - }); - } - - function clearEvents() { - session.clearMessages(); - } - } - function getNodeModuleDirectories(dir: string) { return getRootsToWatchWithAncestorDirectory(dir, nodeModules); } @@ -57,23 +15,6 @@ namespace ts.projectSystem { checkArray("ScriptInfos files", arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles); } - function protocolLocationFromSubstring(str: string, substring: string): protocol.Location { - const start = str.indexOf(substring); - Debug.assert(start !== -1); - return protocolToLocation(str)(start); - } - function protocolToLocation(text: string): (pos: number) => protocol.Location { - const lineStarts = computeLineStarts(text); - return pos => { - const x = computeLineAndCharacterOfPosition(lineStarts, pos); - return { line: x.line + 1, offset: x.character + 1 }; - }; - } - function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan { - const span = textSpanFromSubstring(str, substring, options); - const toLocation = protocolToLocation(str); - return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) }; - } function protocolRenameSpanFromSubstring( str: string, substring: string, @@ -82,11 +23,6 @@ namespace ts.projectSystem { ): protocol.RenameTextSpan { return { ...protocolTextSpanFromSubstring(str, substring, options), ...prefixSuffixText }; } - function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan { - const start = nthIndexOf(str, substring, options ? options.index : 0); - Debug.assert(start !== -1); - return createTextSpan(start, substring.length); - } function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs { return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) }; } @@ -99,18 +35,6 @@ namespace ts.projectSystem { function renameLocation(file: File, substring: string, options?: SpanFromSubstringOptions): RenameLocation { return documentSpanFromSubstring(file, substring, options); } - interface SpanFromSubstringOptions { - readonly index: number; - } - - function nthIndexOf(str: string, substr: string, n: number): number { - let index = -1; - for (; n >= 0; n--) { - index = str.indexOf(substr, index + 1); - if (index === -1) return -1; - } - return index; - } interface ErrorInformation { diagnosticMessage: DiagnosticMessage; @@ -7843,198 +7767,6 @@ new C();` }); }); - describe("tsserverProjectSystem ProjectLoadingStart and ProjectLoadingFinish events", () => { - const projectRoot = "/user/username/projects"; - const aTs: File = { - path: `${projectRoot}/a/a.ts`, - content: "export class A { }" - }; - const configA: File = { - path: `${projectRoot}/a/tsconfig.json`, - content: "{}" - }; - const bTsPath = `${projectRoot}/b/b.ts`; - const configBPath = `${projectRoot}/b/tsconfig.json`; - const files = [libFile, aTs, configA]; - - function verifyProjectLoadingStartAndFinish(createSession: (host: TestServerHost) => { - session: TestSession; - getNumberOfEvents: () => number; - clearEvents: () => void; - verifyProjectLoadEvents: (expected: [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]) => void; - }) { - function createSessionToVerifyEvent(files: ReadonlyArray) { - const host = createServerHost(files); - const originalReadFile = host.readFile; - const { session, getNumberOfEvents, clearEvents, verifyProjectLoadEvents } = createSession(host); - host.readFile = file => { - if (file === configA.path || file === configBPath) { - assert.equal(getNumberOfEvents(), 1, "Event for loading is sent before reading config file"); - } - return originalReadFile.call(host, file); - }; - const service = session.getProjectService(); - return { host, session, verifyEvent, verifyEventWithOpenTs, service, getNumberOfEvents }; - - function verifyEvent(project: server.Project, reason: string) { - verifyProjectLoadEvents([ - { eventName: server.ProjectLoadingStartEvent, data: { project, reason } }, - { eventName: server.ProjectLoadingFinishEvent, data: { project } } - ]); - clearEvents(); - } - - function verifyEventWithOpenTs(file: File, configPath: string, configuredProjects: number) { - openFilesForSession([file], session); - checkNumberOfProjects(service, { configuredProjects }); - const project = service.configuredProjects.get(configPath)!; - assert.isDefined(project); - verifyEvent(project, `Creating possible configured project for ${file.path} to open`); - } - } - - it("when project is created by open file", () => { - const bTs: File = { - path: bTsPath, - content: "export class B {}" - }; - const configB: File = { - path: configBPath, - content: "{}" - }; - const { verifyEventWithOpenTs } = createSessionToVerifyEvent(files.concat(bTs, configB)); - verifyEventWithOpenTs(aTs, configA.path, 1); - verifyEventWithOpenTs(bTs, configB.path, 2); - }); - - it("when change is detected in the config file", () => { - const { host, verifyEvent, verifyEventWithOpenTs, service } = createSessionToVerifyEvent(files); - verifyEventWithOpenTs(aTs, configA.path, 1); - - host.writeFile(configA.path, configA.content); - host.checkTimeoutQueueLengthAndRun(2); - const project = service.configuredProjects.get(configA.path)!; - verifyEvent(project, `Change in config file detected`); - }); - - it("when opening original location project", () => { - const aDTs: File = { - path: `${projectRoot}/a/a.d.ts`, - content: `export declare class A { -} -//# sourceMappingURL=a.d.ts.map -` - }; - const aDTsMap: File = { - path: `${projectRoot}/a/a.d.ts.map`, - content: `{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["./a.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;CAAI"}` - }; - const bTs: File = { - path: bTsPath, - content: `import {A} from "../a/a"; new A();` - }; - const configB: File = { - path: configBPath, - content: JSON.stringify({ - references: [{ path: "../a" }] - }) - }; - - const { service, session, verifyEventWithOpenTs, verifyEvent } = createSessionToVerifyEvent(files.concat(aDTs, aDTsMap, bTs, configB)); - verifyEventWithOpenTs(bTs, configB.path, 1); - - session.executeCommandSeq({ - command: protocol.CommandTypes.References, - arguments: { - file: bTs.path, - ...protocolLocationFromSubstring(bTs.content, "A()") - } - }); - - checkNumberOfProjects(service, { configuredProjects: 2 }); - const project = service.configuredProjects.get(configA.path)!; - assert.isDefined(project); - verifyEvent(project, `Creating project for original file: ${aTs.path} for location: ${aDTs.path}`); - }); - - describe("with external projects and config files ", () => { - const projectFileName = `${projectRoot}/a/project.csproj`; - - function createSession(lazyConfiguredProjectsFromExternalProject: boolean) { - const { session, service, verifyEvent: verifyEventWorker, getNumberOfEvents } = createSessionToVerifyEvent(files); - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - service.openExternalProject({ - projectFileName, - rootFiles: toExternalFiles([aTs.path, configA.path]), - options: {} - }); - checkNumberOfProjects(service, { configuredProjects: 1 }); - return { session, service, verifyEvent, getNumberOfEvents }; - - function verifyEvent() { - const projectA = service.configuredProjects.get(configA.path)!; - assert.isDefined(projectA); - verifyEventWorker(projectA, `Creating configured project in external project: ${projectFileName}`); - } - } - - it("when lazyConfiguredProjectsFromExternalProject is false", () => { - const { verifyEvent } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ false); - verifyEvent(); - }); - - it("when lazyConfiguredProjectsFromExternalProject is true and file is opened", () => { - const { verifyEvent, getNumberOfEvents, session } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true); - assert.equal(getNumberOfEvents(), 0); - - openFilesForSession([aTs], session); - verifyEvent(); - }); - - it("when lazyConfiguredProjectsFromExternalProject is disabled", () => { - const { verifyEvent, getNumberOfEvents, service } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true); - assert.equal(getNumberOfEvents(), 0); - - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); - verifyEvent(); - }); - }); - } - - describe("when using event handler", () => { - verifyProjectLoadingStartAndFinish(host => { - const { session, events } = createSessionWithEventTracking(host, server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent); - return { - session, - getNumberOfEvents: () => events.length, - clearEvents: () => events.length = 0, - verifyProjectLoadEvents: expected => assert.deepEqual(events, expected) - }; - }); - }); - - describe("when using default event handler", () => { - verifyProjectLoadingStartAndFinish(host => { - const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler(host, [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]); - return { - session, - getNumberOfEvents: () => getEvents().length, - clearEvents, - verifyProjectLoadEvents - }; - - function verifyProjectLoadEvents(expected: [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]) { - const actual = getEvents().map(e => ({ eventName: e.event, data: e.body })); - const mappedExpected = expected.map(e => { - const { project, ...rest } = e.data; - return { eventName: e.eventName, data: { projectName: project.getProjectName(), ...rest } }; - }); - assert.deepEqual(actual, mappedExpected); - } - }); - }); - }); - describe("tsserverProjectSystem syntax operations", () => { function navBarFull(session: TestSession, file: File) { return JSON.stringify(session.executeCommandSeq({ From 04e190abb7a5bd899c32116c3b383334a26609b3 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 12:43:07 -0800 Subject: [PATCH 10/20] ProjectUpdatedInBackground in a separate unittest --- src/testRunner/tsconfig.json | 1 + .../unittests/tsserverProjectSystem.ts | 590 ----------------- ...tsserverProjectUpdatedInBackgroundEvent.ts | 591 ++++++++++++++++++ 3 files changed, 592 insertions(+), 590 deletions(-) create mode 100644 src/testRunner/unittests/tsserverProjectUpdatedInBackgroundEvent.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 3b8308af890..6c18df789f6 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -90,6 +90,7 @@ "unittests/tscWatchMode.ts", "unittests/tsserverProjectLoadingEvents.ts", "unittests/tsserverProjectSystem.ts", + "unittests/tsserverProjectUpdatedInBackgroundEvent.ts", "unittests/typingsInstaller.ts", "unittests/versionCache.ts", "unittests/watchEnvironment.ts", diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 74522bcca8d..c098eede598 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -6580,596 +6580,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem ProjectsChangedInBackground", () => { - function verifyFiles(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { - assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`); - const seen = createMap(); - forEach(actual, f => { - assert.isFalse(seen.has(f), `${caption}: Found duplicate ${f}. Actual: ${actual} Expected: ${expected}`); - seen.set(f, true); - assert.isTrue(contains(expected, f), `${caption}: Expected not to contain ${f}. Actual: ${actual} Expected: ${expected}`); - }); - } - - function createVerifyInitialOpen(session: TestSession, verifyProjectsUpdatedInBackgroundEventHandler: (events: server.ProjectsUpdatedInBackgroundEvent[]) => void) { - return (file: File) => { - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { - file: file.path - } - }); - verifyProjectsUpdatedInBackgroundEventHandler([]); - }; - } - - interface ProjectsUpdatedInBackgroundEventVerifier { - session: TestSession; - verifyProjectsUpdatedInBackgroundEventHandler(events: server.ProjectsUpdatedInBackgroundEvent[]): void; - verifyInitialOpen(file: File): void; - } - - function verifyProjectsUpdatedInBackgroundEvent(createSession: (host: TestServerHost) => ProjectsUpdatedInBackgroundEventVerifier) { - it("when adding new file", () => { - const commonFile1: File = { - path: "/a/b/file1.ts", - content: "export var x = 10;" - }; - const commonFile2: File = { - path: "/a/b/file2.ts", - content: "export var y = 10;" - }; - const commonFile3: File = { - path: "/a/b/file3.ts", - content: "export var z = 10;" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const openFiles = [commonFile1.path]; - const host = createServerHost([commonFile1, libFile, configFile]); - const { verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); - verifyInitialOpen(commonFile1); - - host.reloadFS([commonFile1, libFile, configFile, commonFile2]); - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: server.ProjectsUpdatedInBackgroundEvent, - data: { - openFiles - } - }]); - - host.reloadFS([commonFile1, commonFile2, libFile, configFile, commonFile3]); - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: server.ProjectsUpdatedInBackgroundEvent, - data: { - openFiles - } - }]); - }); - - describe("with --out or --outFile setting", () => { - function verifyEventWithOutSettings(compilerOptions: CompilerOptions = {}) { - const config: File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions - }) - }; - - const f1: File = { - path: "/a/a.ts", - content: "export let x = 1" - }; - const f2: File = { - path: "/a/b.ts", - content: "export let y = 1" - }; - - const openFiles = [f1.path]; - const files = [f1, config, libFile]; - const host = createServerHost(files); - const { verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); - verifyInitialOpen(f1); - - files.push(f2); - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: server.ProjectsUpdatedInBackgroundEvent, - data: { - openFiles - } - }]); - - f2.content = "export let x = 11"; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: server.ProjectsUpdatedInBackgroundEvent, - data: { - openFiles - } - }]); - } - - it("when both options are not set", () => { - verifyEventWithOutSettings(); - }); - - it("when --out is set", () => { - const outJs = "/a/out.js"; - verifyEventWithOutSettings({ out: outJs }); - }); - - it("when --outFile is set", () => { - const outJs = "/a/out.js"; - verifyEventWithOutSettings({ outFile: outJs }); - }); - }); - - describe("with modules and configured project", () => { - const file1Consumer1Path = "/a/b/file1Consumer1.ts"; - const moduleFile1Path = "/a/b/moduleFile1.ts"; - const configFilePath = "/a/b/tsconfig.json"; - interface InitialStateParams { - /** custom config file options */ - configObj?: any; - /** Additional files and folders to add */ - getAdditionalFileOrFolder?(): File[]; - /** initial list of files to reload in fs and first file in this list being the file to open */ - firstReloadFileList?: string[]; - } - function getInitialState({ configObj = {}, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { - const moduleFile1: File = { - path: moduleFile1Path, - content: "export function Foo() { };", - }; - - const file1Consumer1: File = { - path: file1Consumer1Path, - content: `import {Foo} from "./moduleFile1"; export var y = 10;`, - }; - - const file1Consumer2: File = { - path: "/a/b/file1Consumer2.ts", - content: `import {Foo} from "./moduleFile1"; let z = 10;`, - }; - - const moduleFile2: File = { - path: "/a/b/moduleFile2.ts", - content: `export var Foo4 = 10;`, - }; - - const globalFile3: File = { - path: "/a/b/globalFile3.ts", - content: `interface GlobalFoo { age: number }` - }; - - const additionalFiles = getAdditionalFileOrFolder ? getAdditionalFileOrFolder() : []; - const configFile = { - path: configFilePath, - content: JSON.stringify(configObj || { compilerOptions: {} }) - }; - - const files: File[] = [file1Consumer1, moduleFile1, file1Consumer2, moduleFile2, ...additionalFiles, globalFile3, libFile, configFile]; - - const filesToReload = firstReloadFileList && getFiles(firstReloadFileList) || files; - const host = createServerHost([filesToReload[0], configFile]); - - // Initial project creation - const { session, verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); - const openFiles = [filesToReload[0].path]; - verifyInitialOpen(filesToReload[0]); - - // Since this is first event, it will have all the files - verifyProjectsUpdatedInBackgroundEvent(filesToReload); - - return { - moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, - files, - updateContentOfOpenFile, - verifyNoProjectsUpdatedInBackgroundEvent, - verifyProjectsUpdatedInBackgroundEvent - }; - - function getFiles(filelist: string[]) { - return map(filelist, getFile); - } - - function getFile(fileName: string) { - return find(files, file => file.path === fileName)!; - } - - function verifyNoProjectsUpdatedInBackgroundEvent(filesToReload?: File[]) { - host.reloadFS(filesToReload || files); - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([]); - } - - function verifyProjectsUpdatedInBackgroundEvent(filesToReload?: File[]) { - host.reloadFS(filesToReload || files); - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: server.ProjectsUpdatedInBackgroundEvent, - data: { - openFiles - } - }]); - } - - function updateContentOfOpenFile(file: File, newContent: string) { - session.executeCommandSeq({ - command: server.CommandNames.Change, - arguments: { - file: file.path, - insertString: newContent, - endLine: 1, - endOffset: file.content.length, - line: 1, - offset: 1 - } - }); - file.content = newContent; - } - } - - it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { - const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` - moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; - verifyProjectsUpdatedInBackgroundEvent(); - }); - - it("should be up-to-date with the reference map changes", () => { - const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyProjectsUpdatedInBackgroundEvent, verifyNoProjectsUpdatedInBackgroundEvent } = getInitialState(); - - // Change file1Consumer1 content to `export let y = Foo();` - updateContentOfOpenFile(file1Consumer1, "export let y = Foo();"); - verifyNoProjectsUpdatedInBackgroundEvent(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - - // Add the import statements back to file1Consumer1 - updateContentOfOpenFile(file1Consumer1, `import {Foo} from "./moduleFile1";let y = Foo();`); - verifyNoProjectsUpdatedInBackgroundEvent(); - - // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` - moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - - // Multiple file edits in one go: - - // Change file1Consumer1 content to `export let y = Foo();` - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - updateContentOfOpenFile(file1Consumer1, `export let y = Foo();`); - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - }); - - it("should be up-to-date with deleted files", () => { - const { moduleFile1, file1Consumer2, files, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - - // Delete file1Consumer2 - const filesToLoad = filter(files, file => file !== file1Consumer2); - verifyProjectsUpdatedInBackgroundEvent(filesToLoad); - }); - - it("should be up-to-date with newly created files", () => { - const { moduleFile1, files, verifyProjectsUpdatedInBackgroundEvent, } = getInitialState(); - - const file1Consumer3: File = { - path: "/a/b/file1Consumer3.ts", - content: `import {Foo} from "./moduleFile1"; let y = Foo();` - }; - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(files.concat(file1Consumer3)); - }); - - it("should detect changes in non-root files", () => { - const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - configObj: { files: [file1Consumer1Path] }, - }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - - // change file1 internal, and verify only file1 is affected - moduleFile1.content += "var T1: number;"; - verifyProjectsUpdatedInBackgroundEvent(); - }); - - it("should return all files if a global file changed shape", () => { - const { globalFile3, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); - - globalFile3.content += "var T2: string;"; - verifyProjectsUpdatedInBackgroundEvent(); - }); - - it("should always return the file itself if '--isolatedModules' is specified", () => { - const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - configObj: { compilerOptions: { isolatedModules: true } } - }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - }); - - it("should always return the file itself if '--out' or '--outFile' is specified", () => { - const outFilePath = "/a/b/out.js"; - const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - configObj: { compilerOptions: { module: "system", outFile: outFilePath } } - }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - }); - - it("should return cascaded affected file list", () => { - const file1Consumer1Consumer1: File = { - path: "/a/b/file1Consumer1Consumer1.ts", - content: `import {y} from "./file1Consumer1";` - }; - const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] - }); - - updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;"); - verifyNoProjectsUpdatedInBackgroundEvent(); - - // Doesnt change the shape of file1Consumer1 - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - - // Change both files before the timeout - updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;"); - moduleFile1.content = `export var T2: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - }); - - it("should work fine for files with circular references", () => { - const file1: File = { - path: "/a/b/file1.ts", - content: ` - /// - export var t1 = 10;` - }; - const file2: File = { - path: "/a/b/file2.ts", - content: ` - /// - export var t2 = 10;` - }; - const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - getAdditionalFileOrFolder: () => [file1, file2], - firstReloadFileList: [file1.path, libFile.path, file2.path, configFilePath] - }); - - file2.content += "export var t3 = 10;"; - verifyProjectsUpdatedInBackgroundEvent([file1, file2, libFile, configFile]); - }); - - it("should detect removed code file", () => { - const referenceFile1: File = { - path: "/a/b/referenceFile1.ts", - content: ` - /// - export var x = Foo();` - }; - const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - getAdditionalFileOrFolder: () => [referenceFile1], - firstReloadFileList: [referenceFile1.path, libFile.path, moduleFile1Path, configFilePath] - }); - - verifyProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]); - }); - - it("should detect non-existing code file", () => { - const referenceFile1: File = { - path: "/a/b/referenceFile1.ts", - content: ` - /// - export var x = Foo();` - }; - const { configFile, moduleFile2, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - getAdditionalFileOrFolder: () => [referenceFile1], - firstReloadFileList: [referenceFile1.path, libFile.path, configFilePath] - }); - - updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();"); - verifyNoProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]); - - // Create module File2 and see both files are saved - verifyProjectsUpdatedInBackgroundEvent([libFile, moduleFile2, referenceFile1, configFile]); - }); - }); - - describe("resolution when resolution cache size", () => { - function verifyWithMaxCacheLimit(limitHit: boolean, useSlashRootAsSomeNotRootFolderInUserDirectory: boolean) { - const rootFolder = useSlashRootAsSomeNotRootFolderInUserDirectory ? "/user/username/rootfolder/otherfolder/" : "/"; - const file1: File = { - path: rootFolder + "a/b/project/file1.ts", - content: 'import a from "file2"' - }; - const file2: File = { - path: rootFolder + "a/b/node_modules/file2.d.ts", - content: "export class a { }" - }; - const file3: File = { - path: rootFolder + "a/b/project/file3.ts", - content: "export class c { }" - }; - const configFile: File = { - path: rootFolder + "a/b/project/tsconfig.json", - content: JSON.stringify({ compilerOptions: { typeRoots: [] } }) - }; - - const projectFiles = [file1, file3, libFile, configFile]; - const openFiles = [file1.path]; - const watchedRecursiveDirectories = useSlashRootAsSomeNotRootFolderInUserDirectory ? - // Folders of node_modules lookup not in changedRoot - ["a/b/project", "a/b/project/node_modules", "a/b/node_modules", "a/node_modules", "node_modules"].map(v => rootFolder + v) : - // Folder of tsconfig - ["/a/b/project", "/a/b/project/node_modules"]; - const host = createServerHost(projectFiles); - const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); - const projectService = session.getProjectService(); - verifyInitialOpen(file1); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(configFile.path)!; - verifyProject(); - if (limitHit) { - (project as ResolutionCacheHost).maxNumberOfFilesToIterateForInvalidation = 1; - } - - file3.content += "export class d {}"; - host.reloadFS(projectFiles); - host.checkTimeoutQueueLengthAndRun(2); - - // Since this is first event - verifyProject(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: server.ProjectsUpdatedInBackgroundEvent, - data: { - openFiles - } - }]); - - projectFiles.push(file2); - host.reloadFS(projectFiles); - host.runQueuedTimeoutCallbacks(); - if (useSlashRootAsSomeNotRootFolderInUserDirectory) { - watchedRecursiveDirectories.length = 3; - } - else { - // file2 addition wont be detected - projectFiles.pop(); - assert.isTrue(host.fileExists(file2.path)); - } - verifyProject(); - - verifyProjectsUpdatedInBackgroundEventHandler(useSlashRootAsSomeNotRootFolderInUserDirectory ? [{ - eventName: server.ProjectsUpdatedInBackgroundEvent, - data: { - openFiles - } - }] : []); - - function verifyProject() { - checkProjectActualFiles(project, map(projectFiles, file => file.path)); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); - } - } - - 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 and project is at root level", () => { - verifyWithMaxCacheLimit(/*limitHit*/ true, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); - }); - }); - } - - describe("when event handler is set in the session", () => { - verifyProjectsUpdatedInBackgroundEvent(createSessionWithProjectChangedEventHandler); - - function createSessionWithProjectChangedEventHandler(host: TestServerHost): ProjectsUpdatedInBackgroundEventVerifier { - const { session, events: projectChangedEvents } = createSessionWithEventTracking(host, server.ProjectsUpdatedInBackgroundEvent); - return { - session, - verifyProjectsUpdatedInBackgroundEventHandler, - verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) - }; - - function eventToString(event: server.ProjectsUpdatedInBackgroundEvent) { - return JSON.stringify(event && { eventName: event.eventName, data: event.data }); - } - - function eventsToString(events: ReadonlyArray) { - return "[" + map(events, eventToString).join(",") + "]"; - } - - function verifyProjectsUpdatedInBackgroundEventHandler(expectedEvents: ReadonlyArray) { - assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`); - forEach(projectChangedEvents, (actualEvent, i) => { - const expectedEvent = expectedEvents[i]; - assert.strictEqual(actualEvent.eventName, expectedEvent.eventName); - verifyFiles("openFiles", actualEvent.data.openFiles, expectedEvent.data.openFiles); - }); - - // Verified the events, reset them - projectChangedEvents.length = 0; - } - } - }); - - describe("when event handler is not set but session is created with canUseEvents = true", () => { - describe("without noGetErrOnBackgroundUpdate, diagnostics for open files are queued", () => { - verifyProjectsUpdatedInBackgroundEvent(createSessionThatUsesEvents); - }); - - describe("with noGetErrOnBackgroundUpdate, diagnostics for open file are not queued", () => { - verifyProjectsUpdatedInBackgroundEvent(host => createSessionThatUsesEvents(host, /*noGetErrOnBackgroundUpdate*/ true)); - }); - - - function createSessionThatUsesEvents(host: TestServerHost, noGetErrOnBackgroundUpdate?: boolean): ProjectsUpdatedInBackgroundEventVerifier { - const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler(host, server.ProjectsUpdatedInBackgroundEvent, { noGetErrOnBackgroundUpdate }); - - return { - session, - verifyProjectsUpdatedInBackgroundEventHandler, - verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) - }; - - function verifyProjectsUpdatedInBackgroundEventHandler(expected: ReadonlyArray) { - const expectedEvents: protocol.ProjectsUpdatedInBackgroundEventBody[] = map(expected, e => { - return { - openFiles: e.data.openFiles - }; - }); - const events = getEvents(); - assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${map(events, e => e.body)} Expected: ${expectedEvents}`); - forEach(events, (actualEvent, i) => { - const expectedEvent = expectedEvents[i]; - verifyFiles("openFiles", actualEvent.body.openFiles, expectedEvent.openFiles); - }); - - // Verified the events, reset them - clearEvents(); - - if (events.length) { - host.checkTimeoutQueueLength(noGetErrOnBackgroundUpdate ? 0 : 1); // Error checking queued only if not noGetErrOnBackgroundUpdate - } - } - } - }); - }); - describe("tsserverProjectSystem with symLinks", () => { it("rename in common file renames all project", () => { const projects = "/users/username/projects"; diff --git a/src/testRunner/unittests/tsserverProjectUpdatedInBackgroundEvent.ts b/src/testRunner/unittests/tsserverProjectUpdatedInBackgroundEvent.ts new file mode 100644 index 00000000000..3e8382fff58 --- /dev/null +++ b/src/testRunner/unittests/tsserverProjectUpdatedInBackgroundEvent.ts @@ -0,0 +1,591 @@ +namespace ts.projectSystem { + describe("tsserverProjectUpdatedInBackgroundEvent:: ProjectsUpdatedInBackground", () => { + function verifyFiles(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { + assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`); + const seen = createMap(); + forEach(actual, f => { + assert.isFalse(seen.has(f), `${caption}: Found duplicate ${f}. Actual: ${actual} Expected: ${expected}`); + seen.set(f, true); + assert.isTrue(contains(expected, f), `${caption}: Expected not to contain ${f}. Actual: ${actual} Expected: ${expected}`); + }); + } + + function createVerifyInitialOpen(session: TestSession, verifyProjectsUpdatedInBackgroundEventHandler: (events: server.ProjectsUpdatedInBackgroundEvent[]) => void) { + return (file: File) => { + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { + file: file.path + } + }); + verifyProjectsUpdatedInBackgroundEventHandler([]); + }; + } + + interface ProjectsUpdatedInBackgroundEventVerifier { + session: TestSession; + verifyProjectsUpdatedInBackgroundEventHandler(events: server.ProjectsUpdatedInBackgroundEvent[]): void; + verifyInitialOpen(file: File): void; + } + + function verifyProjectsUpdatedInBackgroundEvent(createSession: (host: TestServerHost) => ProjectsUpdatedInBackgroundEventVerifier) { + it("when adding new file", () => { + const commonFile1: File = { + path: "/a/b/file1.ts", + content: "export var x = 10;" + }; + const commonFile2: File = { + path: "/a/b/file2.ts", + content: "export var y = 10;" + }; + const commonFile3: File = { + path: "/a/b/file3.ts", + content: "export var z = 10;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const openFiles = [commonFile1.path]; + const host = createServerHost([commonFile1, libFile, configFile]); + const { verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); + verifyInitialOpen(commonFile1); + + host.reloadFS([commonFile1, libFile, configFile, commonFile2]); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + + host.reloadFS([commonFile1, commonFile2, libFile, configFile, commonFile3]); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + }); + + describe("with --out or --outFile setting", () => { + function verifyEventWithOutSettings(compilerOptions: CompilerOptions = {}) { + const config: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions + }) + }; + + const f1: File = { + path: "/a/a.ts", + content: "export let x = 1" + }; + const f2: File = { + path: "/a/b.ts", + content: "export let y = 1" + }; + + const openFiles = [f1.path]; + const files = [f1, config, libFile]; + const host = createServerHost(files); + const { verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); + verifyInitialOpen(f1); + + files.push(f2); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + + f2.content = "export let x = 11"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + } + + it("when both options are not set", () => { + verifyEventWithOutSettings(); + }); + + it("when --out is set", () => { + const outJs = "/a/out.js"; + verifyEventWithOutSettings({ out: outJs }); + }); + + it("when --outFile is set", () => { + const outJs = "/a/out.js"; + verifyEventWithOutSettings({ outFile: outJs }); + }); + }); + + describe("with modules and configured project", () => { + const file1Consumer1Path = "/a/b/file1Consumer1.ts"; + const moduleFile1Path = "/a/b/moduleFile1.ts"; + const configFilePath = "/a/b/tsconfig.json"; + interface InitialStateParams { + /** custom config file options */ + configObj?: any; + /** Additional files and folders to add */ + getAdditionalFileOrFolder?(): File[]; + /** initial list of files to reload in fs and first file in this list being the file to open */ + firstReloadFileList?: string[]; + } + function getInitialState({ configObj = {}, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { + const moduleFile1: File = { + path: moduleFile1Path, + content: "export function Foo() { };", + }; + + const file1Consumer1: File = { + path: file1Consumer1Path, + content: `import {Foo} from "./moduleFile1"; export var y = 10;`, + }; + + const file1Consumer2: File = { + path: "/a/b/file1Consumer2.ts", + content: `import {Foo} from "./moduleFile1"; let z = 10;`, + }; + + const moduleFile2: File = { + path: "/a/b/moduleFile2.ts", + content: `export var Foo4 = 10;`, + }; + + const globalFile3: File = { + path: "/a/b/globalFile3.ts", + content: `interface GlobalFoo { age: number }` + }; + + const additionalFiles = getAdditionalFileOrFolder ? getAdditionalFileOrFolder() : []; + const configFile = { + path: configFilePath, + content: JSON.stringify(configObj || { compilerOptions: {} }) + }; + + const files: File[] = [file1Consumer1, moduleFile1, file1Consumer2, moduleFile2, ...additionalFiles, globalFile3, libFile, configFile]; + + const filesToReload = firstReloadFileList && getFiles(firstReloadFileList) || files; + const host = createServerHost([filesToReload[0], configFile]); + + // Initial project creation + const { session, verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); + const openFiles = [filesToReload[0].path]; + verifyInitialOpen(filesToReload[0]); + + // Since this is first event, it will have all the files + verifyProjectsUpdatedInBackgroundEvent(filesToReload); + + return { + moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, + files, + updateContentOfOpenFile, + verifyNoProjectsUpdatedInBackgroundEvent, + verifyProjectsUpdatedInBackgroundEvent + }; + + function getFiles(filelist: string[]) { + return map(filelist, getFile); + } + + function getFile(fileName: string) { + return find(files, file => file.path === fileName)!; + } + + function verifyNoProjectsUpdatedInBackgroundEvent(filesToReload?: File[]) { + host.reloadFS(filesToReload || files); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([]); + } + + function verifyProjectsUpdatedInBackgroundEvent(filesToReload?: File[]) { + host.reloadFS(filesToReload || files); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + } + + function updateContentOfOpenFile(file: File, newContent: string) { + session.executeCommandSeq({ + command: server.CommandNames.Change, + arguments: { + file: file.path, + insertString: newContent, + endLine: 1, + endOffset: file.content.length, + line: 1, + offset: 1 + } + }); + file.content = newContent; + } + } + + it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` + moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should be up-to-date with the reference map changes", () => { + const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyProjectsUpdatedInBackgroundEvent, verifyNoProjectsUpdatedInBackgroundEvent } = getInitialState(); + + // Change file1Consumer1 content to `export let y = Foo();` + updateContentOfOpenFile(file1Consumer1, "export let y = Foo();"); + verifyNoProjectsUpdatedInBackgroundEvent(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + + // Add the import statements back to file1Consumer1 + updateContentOfOpenFile(file1Consumer1, `import {Foo} from "./moduleFile1";let y = Foo();`); + verifyNoProjectsUpdatedInBackgroundEvent(); + + // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` + moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + + // Multiple file edits in one go: + + // Change file1Consumer1 content to `export let y = Foo();` + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + updateContentOfOpenFile(file1Consumer1, `export let y = Foo();`); + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should be up-to-date with deleted files", () => { + const { moduleFile1, file1Consumer2, files, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + + // Delete file1Consumer2 + const filesToLoad = filter(files, file => file !== file1Consumer2); + verifyProjectsUpdatedInBackgroundEvent(filesToLoad); + }); + + it("should be up-to-date with newly created files", () => { + const { moduleFile1, files, verifyProjectsUpdatedInBackgroundEvent, } = getInitialState(); + + const file1Consumer3: File = { + path: "/a/b/file1Consumer3.ts", + content: `import {Foo} from "./moduleFile1"; let y = Foo();` + }; + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(files.concat(file1Consumer3)); + }); + + it("should detect changes in non-root files", () => { + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + configObj: { files: [file1Consumer1Path] }, + }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + + // change file1 internal, and verify only file1 is affected + moduleFile1.content += "var T1: number;"; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should return all files if a global file changed shape", () => { + const { globalFile3, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); + + globalFile3.content += "var T2: string;"; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should always return the file itself if '--isolatedModules' is specified", () => { + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + configObj: { compilerOptions: { isolatedModules: true } } + }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should always return the file itself if '--out' or '--outFile' is specified", () => { + const outFilePath = "/a/b/out.js"; + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + configObj: { compilerOptions: { module: "system", outFile: outFilePath } } + }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should return cascaded affected file list", () => { + const file1Consumer1Consumer1: File = { + path: "/a/b/file1Consumer1Consumer1.ts", + content: `import {y} from "./file1Consumer1";` + }; + const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] + }); + + updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;"); + verifyNoProjectsUpdatedInBackgroundEvent(); + + // Doesnt change the shape of file1Consumer1 + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + + // Change both files before the timeout + updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;"); + moduleFile1.content = `export var T2: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should work fine for files with circular references", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: ` + /// + export var t1 = 10;` + }; + const file2: File = { + path: "/a/b/file2.ts", + content: ` + /// + export var t2 = 10;` + }; + const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [file1, file2], + firstReloadFileList: [file1.path, libFile.path, file2.path, configFilePath] + }); + + file2.content += "export var t3 = 10;"; + verifyProjectsUpdatedInBackgroundEvent([file1, file2, libFile, configFile]); + }); + + it("should detect removed code file", () => { + const referenceFile1: File = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [referenceFile1.path, libFile.path, moduleFile1Path, configFilePath] + }); + + verifyProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]); + }); + + it("should detect non-existing code file", () => { + const referenceFile1: File = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const { configFile, moduleFile2, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [referenceFile1.path, libFile.path, configFilePath] + }); + + updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();"); + verifyNoProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]); + + // Create module File2 and see both files are saved + verifyProjectsUpdatedInBackgroundEvent([libFile, moduleFile2, referenceFile1, configFile]); + }); + }); + + describe("resolution when resolution cache size", () => { + function verifyWithMaxCacheLimit(limitHit: boolean, useSlashRootAsSomeNotRootFolderInUserDirectory: boolean) { + const rootFolder = useSlashRootAsSomeNotRootFolderInUserDirectory ? "/user/username/rootfolder/otherfolder/" : "/"; + const file1: File = { + path: rootFolder + "a/b/project/file1.ts", + content: 'import a from "file2"' + }; + const file2: File = { + path: rootFolder + "a/b/node_modules/file2.d.ts", + content: "export class a { }" + }; + const file3: File = { + path: rootFolder + "a/b/project/file3.ts", + content: "export class c { }" + }; + const configFile: File = { + path: rootFolder + "a/b/project/tsconfig.json", + content: JSON.stringify({ compilerOptions: { typeRoots: [] } }) + }; + + const projectFiles = [file1, file3, libFile, configFile]; + const openFiles = [file1.path]; + const watchedRecursiveDirectories = useSlashRootAsSomeNotRootFolderInUserDirectory ? + // Folders of node_modules lookup not in changedRoot + ["a/b/project", "a/b/project/node_modules", "a/b/node_modules", "a/node_modules", "node_modules"].map(v => rootFolder + v) : + // Folder of tsconfig + ["/a/b/project", "/a/b/project/node_modules"]; + const host = createServerHost(projectFiles); + const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); + const projectService = session.getProjectService(); + verifyInitialOpen(file1); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path)!; + verifyProject(); + if (limitHit) { + (project as ResolutionCacheHost).maxNumberOfFilesToIterateForInvalidation = 1; + } + + file3.content += "export class d {}"; + host.reloadFS(projectFiles); + host.checkTimeoutQueueLengthAndRun(2); + + // Since this is first event + verifyProject(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + + projectFiles.push(file2); + host.reloadFS(projectFiles); + host.runQueuedTimeoutCallbacks(); + if (useSlashRootAsSomeNotRootFolderInUserDirectory) { + watchedRecursiveDirectories.length = 3; + } + else { + // file2 addition wont be detected + projectFiles.pop(); + assert.isTrue(host.fileExists(file2.path)); + } + verifyProject(); + + verifyProjectsUpdatedInBackgroundEventHandler(useSlashRootAsSomeNotRootFolderInUserDirectory ? [{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }] : []); + + function verifyProject() { + checkProjectActualFiles(project, map(projectFiles, file => file.path)); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + } + } + + 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 and project is at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ true, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); + }); + }); + } + + describe("when event handler is set in the session", () => { + verifyProjectsUpdatedInBackgroundEvent(createSessionWithProjectChangedEventHandler); + + function createSessionWithProjectChangedEventHandler(host: TestServerHost): ProjectsUpdatedInBackgroundEventVerifier { + const { session, events: projectChangedEvents } = createSessionWithEventTracking(host, server.ProjectsUpdatedInBackgroundEvent); + return { + session, + verifyProjectsUpdatedInBackgroundEventHandler, + verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) + }; + + function eventToString(event: server.ProjectsUpdatedInBackgroundEvent) { + return JSON.stringify(event && { eventName: event.eventName, data: event.data }); + } + + function eventsToString(events: ReadonlyArray) { + return "[" + map(events, eventToString).join(",") + "]"; + } + + function verifyProjectsUpdatedInBackgroundEventHandler(expectedEvents: ReadonlyArray) { + assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`); + forEach(projectChangedEvents, (actualEvent, i) => { + const expectedEvent = expectedEvents[i]; + assert.strictEqual(actualEvent.eventName, expectedEvent.eventName); + verifyFiles("openFiles", actualEvent.data.openFiles, expectedEvent.data.openFiles); + }); + + // Verified the events, reset them + projectChangedEvents.length = 0; + } + } + }); + + describe("when event handler is not set but session is created with canUseEvents = true", () => { + describe("without noGetErrOnBackgroundUpdate, diagnostics for open files are queued", () => { + verifyProjectsUpdatedInBackgroundEvent(createSessionThatUsesEvents); + }); + + describe("with noGetErrOnBackgroundUpdate, diagnostics for open file are not queued", () => { + verifyProjectsUpdatedInBackgroundEvent(host => createSessionThatUsesEvents(host, /*noGetErrOnBackgroundUpdate*/ true)); + }); + + + function createSessionThatUsesEvents(host: TestServerHost, noGetErrOnBackgroundUpdate?: boolean): ProjectsUpdatedInBackgroundEventVerifier { + const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler(host, server.ProjectsUpdatedInBackgroundEvent, { noGetErrOnBackgroundUpdate }); + + return { + session, + verifyProjectsUpdatedInBackgroundEventHandler, + verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) + }; + + function verifyProjectsUpdatedInBackgroundEventHandler(expected: ReadonlyArray) { + const expectedEvents: protocol.ProjectsUpdatedInBackgroundEventBody[] = map(expected, e => { + return { + openFiles: e.data.openFiles + }; + }); + const events = getEvents(); + assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${map(events, e => e.body)} Expected: ${expectedEvents}`); + forEach(events, (actualEvent, i) => { + const expectedEvent = expectedEvents[i]; + verifyFiles("openFiles", actualEvent.body.openFiles, expectedEvent.openFiles); + }); + + // Verified the events, reset them + clearEvents(); + + if (events.length) { + host.checkTimeoutQueueLength(noGetErrOnBackgroundUpdate ? 0 : 1); // Error checking queued only if not noGetErrOnBackgroundUpdate + } + } + } + }); + }); +} From f6ac9495962c803b718b88fe87a27fa385bf19f4 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 12:47:08 -0800 Subject: [PATCH 11/20] Separate out LargeFileReferencedEvent test --- src/testRunner/tsconfig.json | 1 + .../tsserverLargeFileReferencedEvent.ts | 76 +++++++++++++++++++ .../unittests/tsserverProjectSystem.ts | 75 ------------------ 3 files changed, 77 insertions(+), 75 deletions(-) create mode 100644 src/testRunner/unittests/tsserverLargeFileReferencedEvent.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 6c18df789f6..ba46706ee16 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -88,6 +88,7 @@ "unittests/tsconfigParsing.ts", "unittests/tscWatchEmit.ts", "unittests/tscWatchMode.ts", + "unittests/tsserverLargeFileReferencedEvent.ts", "unittests/tsserverProjectLoadingEvents.ts", "unittests/tsserverProjectSystem.ts", "unittests/tsserverProjectUpdatedInBackgroundEvent.ts", diff --git a/src/testRunner/unittests/tsserverLargeFileReferencedEvent.ts b/src/testRunner/unittests/tsserverLargeFileReferencedEvent.ts new file mode 100644 index 00000000000..f33f32dfe58 --- /dev/null +++ b/src/testRunner/unittests/tsserverLargeFileReferencedEvent.ts @@ -0,0 +1,76 @@ +namespace ts.projectSystem { + describe("tsserverLargeFileReferencedEvent:: LargeFileReferencedEvent with large file", () => { + const projectRoot = "/user/username/projects/project"; + + function getLargeFile(useLargeTsFile: boolean) { + return `src/large.${useLargeTsFile ? "ts" : "js"}`; + } + + function createSessionWithEventHandler(files: File[], useLargeTsFile: boolean) { + const largeFile: File = { + path: `${projectRoot}/${getLargeFile(useLargeTsFile)}`, + content: "export var x = 10;", + fileSize: server.maxFileSize + 1 + }; + files.push(largeFile); + const host = createServerHost(files); + const { session, events: largeFileReferencedEvents } = createSessionWithEventTracking(host, server.LargeFileReferencedEvent); + + return { session, verifyLargeFile }; + + function verifyLargeFile(project: server.Project) { + checkProjectActualFiles(project, files.map(f => f.path)); + + // large file for non ts file should be empty and for ts file should have content + const service = session.getProjectService(); + const info = service.getScriptInfo(largeFile.path)!; + assert.equal(info.cacheSourceFile!.sourceFile.text, useLargeTsFile ? largeFile.content : ""); + + assert.deepEqual(largeFileReferencedEvents, useLargeTsFile ? emptyArray : [{ + eventName: server.LargeFileReferencedEvent, + data: { file: largeFile.path, fileSize: largeFile.fileSize, maxFileSize: server.maxFileSize } + }]); + } + } + + function verifyLargeFile(useLargeTsFile: boolean) { + it("when large file is included by tsconfig", () => { + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ files: ["src/file.ts", getLargeFile(useLargeTsFile)], compilerOptions: { target: 1, allowJs: true } }) + }; + const files = [file, libFile, tsconfig]; + const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); + const service = session.getProjectService(); + openFilesForSession([file], session); + checkNumberOfProjects(service, { configuredProjects: 1 }); + verifyLargeFile(service.configuredProjects.get(tsconfig.path)!); + }); + + it("when large file is included by module resolution", () => { + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: `export var y = 10;import {x} from "./large"` + }; + const files = [file, libFile]; + const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); + const service = session.getProjectService(); + openFilesForSession([file], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + verifyLargeFile(service.inferredProjects[0]); + }); + } + + describe("large file is ts file", () => { + verifyLargeFile(/*useLargeTsFile*/ true); + }); + + describe("large file is js file", () => { + verifyLargeFile(/*useLargeTsFile*/ false); + }); + }); +} diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index c098eede598..0c6382f13c4 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -7102,81 +7102,6 @@ new C();` }); }); - describe("tsserverProjectSystem with large file", () => { - const projectRoot = "/user/username/projects/project"; - - function getLargeFile(useLargeTsFile: boolean) { - return `src/large.${useLargeTsFile ? "ts" : "js"}`; - } - - function createSessionWithEventHandler(files: File[], useLargeTsFile: boolean) { - const largeFile: File = { - path: `${projectRoot}/${getLargeFile(useLargeTsFile)}`, - content: "export var x = 10;", - fileSize: server.maxFileSize + 1 - }; - files.push(largeFile); - const host = createServerHost(files); - const { session, events: largeFileReferencedEvents } = createSessionWithEventTracking(host, server.LargeFileReferencedEvent); - - return { session, verifyLargeFile }; - - function verifyLargeFile(project: server.Project) { - checkProjectActualFiles(project, files.map(f => f.path)); - - // large file for non ts file should be empty and for ts file should have content - const service = session.getProjectService(); - const info = service.getScriptInfo(largeFile.path)!; - assert.equal(info.cacheSourceFile!.sourceFile.text, useLargeTsFile ? largeFile.content : ""); - - assert.deepEqual(largeFileReferencedEvents, useLargeTsFile ? emptyArray : [{ - eventName: server.LargeFileReferencedEvent, - data: { file: largeFile.path, fileSize: largeFile.fileSize, maxFileSize: server.maxFileSize } - }]); - } - } - - function verifyLargeFile(useLargeTsFile: boolean) { - it("when large file is included by tsconfig", () => { - const file: File = { - path: `${projectRoot}/src/file.ts`, - content: "export var y = 10;" - }; - const tsconfig: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify({ files: ["src/file.ts", getLargeFile(useLargeTsFile)], compilerOptions: { target: 1, allowJs: true } }) - }; - const files = [file, libFile, tsconfig]; - const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); - const service = session.getProjectService(); - openFilesForSession([file], session); - checkNumberOfProjects(service, { configuredProjects: 1 }); - verifyLargeFile(service.configuredProjects.get(tsconfig.path)!); - }); - - it("when large file is included by module resolution", () => { - const file: File = { - path: `${projectRoot}/src/file.ts`, - content: `export var y = 10;import {x} from "./large"` - }; - const files = [file, libFile]; - const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); - const service = session.getProjectService(); - openFilesForSession([file], session); - checkNumberOfProjects(service, { inferredProjects: 1 }); - verifyLargeFile(service.inferredProjects[0]); - }); - } - - describe("large file is ts file", () => { - verifyLargeFile(/*useLargeTsFile*/ true); - }); - - describe("large file is js file", () => { - verifyLargeFile(/*useLargeTsFile*/ false); - }); - }); - describe("tsserverProjectSystem syntax operations", () => { function navBarFull(session: TestSession, file: File) { return JSON.stringify(session.executeCommandSeq({ From 96c73701c16b0d859b8b7983701468548d1b6bbe Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 13:03:13 -0800 Subject: [PATCH 12/20] More refactoring for resloutionCache and project errors --- src/testRunner/unittests/projectErrors.ts | 283 +++++++ src/testRunner/unittests/resolutionCache.ts | 401 ++++++++++ src/testRunner/unittests/tsserverHelpers.ts | 44 +- .../unittests/tsserverProjectSystem.ts | 712 ------------------ 4 files changed, 706 insertions(+), 734 deletions(-) diff --git a/src/testRunner/unittests/projectErrors.ts b/src/testRunner/unittests/projectErrors.ts index 15bbab2a551..9ff7278e78c 100644 --- a/src/testRunner/unittests/projectErrors.ts +++ b/src/testRunner/unittests/projectErrors.ts @@ -563,4 +563,287 @@ namespace ts.projectSystem { assert.isTrue(diagsAfterUpdate.length === 0); }); }); + + describe("tsserver:: Project Errors for Configure file diagnostics events", () => { + function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string): ConfigFileDiagnostic { + const d = Diagnostics.Unknown_compiler_option_0; + const start = configFile.content.indexOf(prop) - 1; // start at "prop" + return { + fileName: configFile.path, + start, + length: prop.length + 2, + messageText: formatStringFromArgs(d.message, [prop]), + category: d.category, + code: d.code, + reportsUnnecessary: undefined + }; + } + + function getFileNotFoundDiagnostic(configFile: File, relativeFileName: string): ConfigFileDiagnostic { + const findString = `{"path":"./${relativeFileName}"}`; + const d = Diagnostics.File_0_not_found; + const start = configFile.content.indexOf(findString); + return { + fileName: configFile.path, + start, + length: findString.length, + messageText: formatStringFromArgs(d.message, [`${getDirectoryPath(configFile.path)}/${relativeFileName}`]), + category: d.category, + code: d.code, + reportsUnnecessary: undefined + }; + } + + it("are generated when the config file has errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "foo": "bar", + "allowJS": true + } + }` + }; + const serverEventManager = new TestServerEventManager([file, libFile, configFile]); + openFilesForSession([file], serverEventManager.session); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ + getUnknownCompilerOptionDiagnostic(configFile, "foo"), + getUnknownCompilerOptionDiagnostic(configFile, "allowJS") + ]); + }); + + it("are generated when the config file doesn't have errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {} + }` + }; + const serverEventManager = new TestServerEventManager([file, libFile, configFile]); + openFilesForSession([file], serverEventManager.session); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray); + }); + + it("are generated when the config file changes", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {} + }` + }; + + const files = [file, libFile, configFile]; + const serverEventManager = new TestServerEventManager(files); + openFilesForSession([file], serverEventManager.session); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray); + + configFile.content = `{ + "compilerOptions": { + "haha": 123 + } + }`; + serverEventManager.host.reloadFS(files); + serverEventManager.host.runQueuedTimeoutCallbacks(); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, [ + getUnknownCompilerOptionDiagnostic(configFile, "haha") + ]); + + configFile.content = `{ + "compilerOptions": {} + }`; + serverEventManager.host.reloadFS(files); + serverEventManager.host.runQueuedTimeoutCallbacks(); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, emptyArray); + }); + + it("are not generated when the config file does not include file opened and config file has errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const file2: File = { + path: "/a/b/test.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "foo": "bar", + "allowJS": true + }, + "files": ["app.ts"] + }` + }; + const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]); + openFilesForSession([file2], serverEventManager.session); + serverEventManager.hasZeroEvent("configFileDiag"); + }); + + it("are not generated when the config file has errors but suppressDiagnosticEvents is true", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "foo": "bar", + "allowJS": true + } + }` + }; + const serverEventManager = new TestServerEventManager([file, libFile, configFile], /*suppressDiagnosticEvents*/ true); + openFilesForSession([file], serverEventManager.session); + serverEventManager.hasZeroEvent("configFileDiag"); + }); + + it("are not generated when the config file does not include file opened and doesnt contain any errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const file2: File = { + path: "/a/b/test.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "files": ["app.ts"] + }` + }; + + const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]); + openFilesForSession([file2], serverEventManager.session); + serverEventManager.hasZeroEvent("configFileDiag"); + }); + + it("contains the project reference errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const noSuchTsconfig = "no-such-tsconfig.json"; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "files": ["app.ts"], + "references": [{"path":"./${noSuchTsconfig}"}] + }` + }; + + const serverEventManager = new TestServerEventManager([file, libFile, configFile]); + openFilesForSession([file], serverEventManager.session); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ + getFileNotFoundDiagnostic(configFile, noSuchTsconfig) + ]); + }); + }); + + describe("tsserver:: Project Errors reports Options Diagnostic locations correctly with changes in configFile contents", () => { + it("when options change", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFileContentBeforeComment = `{`; + const configFileContentComment = ` + // comment`; + const configFileContentAfterComment = ` + "compilerOptions": { + "allowJs": true, + "declaration": true + } + }`; + const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment; + const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment; + + const configFile = { + path: "/a/b/tsconfig.json", + content: configFileContentWithComment + }; + const host = createServerHost([file, libFile, configFile]); + const session = createSession(host); + openFilesForSession([file], session); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const projectName = configuredProjectAt(projectService, 0).getProjectName(); + + const diags = session.executeCommand({ + type: "request", + command: server.CommandNames.SemanticDiagnosticsSync, + seq: 2, + arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } + }).response as ReadonlyArray; + assert.isTrue(diags.length === 2); + + configFile.content = configFileContentWithoutCommentLine; + host.reloadFS([file, configFile]); + + const diagsAfterEdit = session.executeCommand({ + type: "request", + command: server.CommandNames.SemanticDiagnosticsSync, + seq: 2, + arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } + }).response as ReadonlyArray; + assert.isTrue(diagsAfterEdit.length === 2); + + verifyDiagnostic(diags[0], diagsAfterEdit[0]); + verifyDiagnostic(diags[1], diagsAfterEdit[1]); + + function verifyDiagnostic(beforeEditDiag: server.protocol.DiagnosticWithLinePosition, afterEditDiag: server.protocol.DiagnosticWithLinePosition) { + assert.equal(beforeEditDiag.message, afterEditDiag.message); + assert.equal(beforeEditDiag.code, afterEditDiag.code); + assert.equal(beforeEditDiag.category, afterEditDiag.category); + assert.equal(beforeEditDiag.startLocation.line, afterEditDiag.startLocation.line + 1); + assert.equal(beforeEditDiag.startLocation.offset, afterEditDiag.startLocation.offset); + assert.equal(beforeEditDiag.endLocation.line, afterEditDiag.endLocation.line + 1); + assert.equal(beforeEditDiag.endLocation.offset, afterEditDiag.endLocation.offset); + } + }); + }); + + describe("tsserver:: Project Errors with config file change", () => { + it("Updates diagnostics when '--noUnusedLabels' changes", () => { + const aTs: File = { path: "/a.ts", content: "label: while (1) {}" }; + const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`; + const tsconfig: File = { path: "/tsconfig.json", content: options(/*allowUnusedLabels*/ true) }; + + const host = createServerHost([aTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs], session); + + host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ false)); + host.runQueuedTimeoutCallbacks(); + + const response = executeSessionRequest(session, protocol.CommandTypes.SemanticDiagnosticsSync, { file: aTs.path }) as protocol.Diagnostic[] | undefined; + assert.deepEqual(response, [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 + "label".length }, + text: "Unused label.", + category: "error", + code: Diagnostics.Unused_label.code, + relatedInformation: undefined, + reportsUnnecessary: true, + source: undefined, + }, + ]); + }); + }); } diff --git a/src/testRunner/unittests/resolutionCache.ts b/src/testRunner/unittests/resolutionCache.ts index fbc792481fc..ab6c6830618 100644 --- a/src/testRunner/unittests/resolutionCache.ts +++ b/src/testRunner/unittests/resolutionCache.ts @@ -978,4 +978,405 @@ export const x = 10;` }); }); }); + + describe("resolutionCache:: tsserverProjectSystem rename a module file and rename back", () => { + it("should restore the states for inferred projects", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const host = createServerHost([moduleFile, file1]); + const session = createSession(host); + + openFilesForSession([file1], session); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + + const moduleFileOldPath = moduleFile.path; + const moduleFileNewPath = "/a/b/moduleFile1.ts"; + moduleFile.path = moduleFileNewPath; + host.reloadFS([moduleFile, file1]); + host.runQueuedTimeoutCallbacks(); + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } + ]); + assert.equal(diags.length, 1); + + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1]); + host.runQueuedTimeoutCallbacks(); + + // Make a change to trigger the program rebuild + const changeRequest = makeSessionRequest( + server.CommandNames.Change, + { file: file1.path, line: 1, offset: 44, endLine: 1, endOffset: 44, insertString: "\n" } + ); + session.executeCommand(changeRequest); + host.runQueuedTimeoutCallbacks(); + + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + }); + + it("should restore the states for configured projects", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([moduleFile, file1, configFile]); + const session = createSession(host); + + openFilesForSession([file1], session); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + + const moduleFileOldPath = moduleFile.path; + const moduleFileNewPath = "/a/b/moduleFile1.ts"; + moduleFile.path = moduleFileNewPath; + host.reloadFS([moduleFile, file1, configFile]); + host.runQueuedTimeoutCallbacks(); + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } + ]); + + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1, configFile]); + host.runQueuedTimeoutCallbacks(); + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + }); + + it("should property handle missing config files", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + const projectName = "project1"; + const host = createServerHost([f1]); + const projectService = createProjectService(host); + projectService.openExternalProject({ rootFiles: toExternalFiles([f1.path, config.path]), options: {}, projectFileName: projectName }); + + // should have one external project since config file is missing + projectService.checkNumberOfProjects({ externalProjects: 1 }); + + host.reloadFS([f1, config]); + projectService.openExternalProject({ rootFiles: toExternalFiles([f1.path, config.path]), options: {}, projectFileName: projectName }); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + }); + + it("types should load from config file path if config exists", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } }) + }; + const node = { + path: "/a/b/node_modules/@types/node/index.d.ts", + content: "declare var process: any" + }; + const cwd = { + path: "/a/c" + }; + const host = createServerHost([f1, config, node, cwd], { currentDirectory: cwd.path }); + const projectService = createProjectService(host); + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, node.path, config.path]); + }); + }); + + describe("resolutionCache:: tsserverProjectSystem add the missing module file for inferred project", () => { + it("should remove the `module not found` error", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const host = createServerHost([file1]); + const session = createSession(host); + openFilesForSession([file1], session); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } + ]); + + host.reloadFS([file1, moduleFile]); + host.runQueuedTimeoutCallbacks(); + + // Make a change to trigger the program rebuild + const changeRequest = makeSessionRequest( + server.CommandNames.Change, + { file: file1.path, line: 1, offset: 44, endLine: 1, endOffset: 44, insertString: "\n" } + ); + session.executeCommand(changeRequest); + + // Recheck + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + }); + + it("npm install @types works", () => { + const folderPath = "/a/b/projects/temp"; + const file1: File = { + path: `${folderPath}/a.ts`, + content: 'import f = require("pad"); f;' + }; + const files = [file1, libFile]; + const host = createServerHost(files); + const session = createSession(host, { canUseEvents: true }); + const service = session.getProjectService(); + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { + file: file1.path, + fileContent: file1.content, + scriptKindName: "TS", + projectRootPath: folderPath + } + }); + checkNumberOfProjects(service, { inferredProjects: 1 }); + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [file1.path] + } + }); + + host.checkTimeoutQueueLengthAndRun(1); + checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(); + const startOffset = file1.content.indexOf('"') + 1; + checkErrorMessage(session, "semanticDiag", { + file: file1.path, + diagnostics: [ + createDiagnostic({ line: 1, offset: startOffset }, { line: 1, offset: startOffset + '"pad"'.length }, Diagnostics.Cannot_find_module_0, ["pad"]) + ], + }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + checkErrorMessage(session, "suggestionDiag", { file: file1.path, diagnostics: [] }); + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); + + const padIndex: File = { + path: `${folderPath}/node_modules/@types/pad/index.d.ts`, + content: "export = pad;declare function pad(length: number, text: string, char ?: string): string;" + }; + files.push(padIndex); + host.reloadFS(files, { ignoreWatchInvokedWithTriggerAsFileCreate: true }); + host.runQueuedTimeoutCallbacks(); + checkProjectUpdatedInBackgroundEvent(session, [file1.path]); + session.clearMessages(); + + host.runQueuedTimeoutCallbacks(); + checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(); + checkErrorMessage(session, "semanticDiag", { file: file1.path, diagnostics: [] }); + }); + + it("suggestion diagnostics", () => { + const file: File = { + path: "/a.js", + content: "function f(p) {}", + }; + + const host = createServerHost([file]); + const session = createSession(host, { canUseEvents: true }); + const service = session.getProjectService(); + + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { file: file.path, fileContent: file.content }, + }); + + checkNumberOfProjects(service, { inferredProjects: 1 }); + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + host.checkTimeoutQueueLengthAndRun(2); + + checkProjectUpdatedInBackgroundEvent(session, [file.path]); + session.clearMessages(); + + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [file.path], + } + }); + + host.checkTimeoutQueueLengthAndRun(1); + + checkErrorMessage(session, "syntaxDiag", { file: file.path, diagnostics: [] }, /*isMostRecent*/ true); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + + checkErrorMessage(session, "semanticDiag", { file: file.path, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + + checkErrorMessage(session, "suggestionDiag", { + file: file.path, + diagnostics: [ + createDiagnostic({ line: 1, offset: 12 }, { line: 1, offset: 13 }, Diagnostics._0_is_declared_but_its_value_is_never_read, ["p"], "suggestion", /*reportsUnnecessary*/ true), + ], + }); + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); + }); + + it("disable suggestion diagnostics", () => { + const file: File = { + path: "/a.js", + content: 'require("b")', + }; + + const host = createServerHost([file]); + const session = createSession(host, { canUseEvents: true }); + const service = session.getProjectService(); + + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { file: file.path, fileContent: file.content }, + }); + + session.executeCommandSeq({ + command: server.CommandNames.Configure, + arguments: { + preferences: { disableSuggestions: true } + }, + }); + + checkNumberOfProjects(service, { inferredProjects: 1 }); + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + host.checkTimeoutQueueLengthAndRun(2); + + checkProjectUpdatedInBackgroundEvent(session, [file.path]); + session.clearMessages(); + + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [file.path], + } + }); + + host.checkTimeoutQueueLengthAndRun(1); + + checkErrorMessage(session, "syntaxDiag", { file: file.path, diagnostics: [] }, /*isMostRecent*/ true); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + + checkErrorMessage(session, "semanticDiag", { file: file.path, diagnostics: [] }); + // No suggestion event, we're done. + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); + }); + + it("suppressed diagnostic events", () => { + const file: File = { + path: "/a.ts", + content: "1 = 2;", + }; + + const host = createServerHost([file]); + const session = createSession(host, { canUseEvents: true, suppressDiagnosticEvents: true }); + const service = session.getProjectService(); + + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { file: file.path, fileContent: file.content }, + }); + + checkNumberOfProjects(service, { inferredProjects: 1 }); + + host.checkTimeoutQueueLength(0); + checkNoDiagnosticEvents(session); + + session.clearMessages(); + + let expectedSequenceId = session.getNextSeq(); + + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [file.path], + } + }); + + host.checkTimeoutQueueLength(0); + checkNoDiagnosticEvents(session); + + checkCompleteEvent(session, 1, expectedSequenceId); + + session.clearMessages(); + + expectedSequenceId = session.getNextSeq(); + + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + file: file.path, + } + }); + + host.checkTimeoutQueueLength(0); + checkNoDiagnosticEvents(session); + + checkCompleteEvent(session, 1, expectedSequenceId); + + session.clearMessages(); + }); + }); } diff --git a/src/testRunner/unittests/tsserverHelpers.ts b/src/testRunner/unittests/tsserverHelpers.ts index 18e6f5c6194..b6154f46b71 100644 --- a/src/testRunner/unittests/tsserverHelpers.ts +++ b/src/testRunner/unittests/tsserverHelpers.ts @@ -599,23 +599,23 @@ namespace ts.projectSystem { } } - //interface ErrorInformation { - // diagnosticMessage: DiagnosticMessage; - // errorTextArguments?: string[]; - //} + export interface ErrorInformation { + diagnosticMessage: DiagnosticMessage; + errorTextArguments?: string[]; + } - //function getProtocolDiagnosticMessage({ diagnosticMessage, errorTextArguments = [] }: ErrorInformation) { - // return formatStringFromArgs(diagnosticMessage.message, errorTextArguments); - //} + function getProtocolDiagnosticMessage({ diagnosticMessage, errorTextArguments = [] }: ErrorInformation) { + return formatStringFromArgs(diagnosticMessage.message, errorTextArguments); + } - //function verifyDiagnostics(actual: server.protocol.Diagnostic[], expected: ErrorInformation[]) { - // const expectedErrors = expected.map(getProtocolDiagnosticMessage); - // assert.deepEqual(actual.map(diag => flattenDiagnosticMessageText(diag.text, "\n")), expectedErrors); - //} + export function verifyDiagnostics(actual: ReadonlyArray, expected: ReadonlyArray) { + const expectedErrors = expected.map(getProtocolDiagnosticMessage); + assert.deepEqual(actual.map(diag => flattenDiagnosticMessageText(diag.text, "\n")), expectedErrors); + } - //function verifyNoDiagnostics(actual: server.protocol.Diagnostic[]) { - // verifyDiagnostics(actual, []); - //} + export function verifyNoDiagnostics(actual: server.protocol.Diagnostic[]) { + verifyDiagnostics(actual, []); + } export function checkErrorMessage(session: TestSession, eventName: protocol.DiagnosticEventKind, diagnostics: protocol.DiagnosticEventBody, isMostRecent = false): void { checkNthEvent(session, server.toEvent(eventName, diagnostics), 0, isMostRecent); @@ -629,15 +629,15 @@ namespace ts.projectSystem { checkNthEvent(session, server.toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, isMostRecent); } - //function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) { - // checkNthEvent(session, server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); - //} + export function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) { + checkNthEvent(session, server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); + } - //function checkNoDiagnosticEvents(session: TestSession) { - // for (const event of session.events) { - // assert.isFalse(event.event.endsWith("Diag"), JSON.stringify(event)); - // } - //} + export function checkNoDiagnosticEvents(session: TestSession) { + for (const event of session.events) { + assert.isFalse(event.event.endsWith("Diag"), JSON.stringify(event)); + } + } export function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { const events = session.events; diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 0c6382f13c4..624b11d8d99 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -36,34 +36,6 @@ namespace ts.projectSystem { return documentSpanFromSubstring(file, substring, options); } - interface ErrorInformation { - diagnosticMessage: DiagnosticMessage; - errorTextArguments?: string[]; - } - - function getProtocolDiagnosticMessage({ diagnosticMessage, errorTextArguments = [] }: ErrorInformation) { - return formatStringFromArgs(diagnosticMessage.message, errorTextArguments); - } - - function verifyDiagnostics(actual: server.protocol.Diagnostic[], expected: ErrorInformation[]) { - const expectedErrors = expected.map(getProtocolDiagnosticMessage); - assert.deepEqual(actual.map(diag => flattenDiagnosticMessageText(diag.text, "\n")), expectedErrors); - } - - function verifyNoDiagnostics(actual: server.protocol.Diagnostic[]) { - verifyDiagnostics(actual, []); - } - - function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) { - checkNthEvent(session, server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); - } - - function checkNoDiagnosticEvents(session: TestSession) { - for (const event of session.events) { - assert.isFalse(event.event.endsWith("Diag"), JSON.stringify(event)); - } - } - describe("tsserverProjectSystem general functionality", () => { const commonFile1: File = { path: "/a/b/commonFile1.ts", @@ -3860,597 +3832,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem rename a module file and rename back", () => { - it("should restore the states for inferred projects", () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: "import * as T from './moduleFile'; T.bar();" - }; - const host = createServerHost([moduleFile, file1]); - const session = createSession(host); - - openFilesForSession([file1], session); - const getErrRequest = makeSessionRequest( - server.CommandNames.SemanticDiagnosticsSync, - { file: file1.path } - ); - let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - - const moduleFileOldPath = moduleFile.path; - const moduleFileNewPath = "/a/b/moduleFile1.ts"; - moduleFile.path = moduleFileNewPath; - host.reloadFS([moduleFile, file1]); - host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyDiagnostics(diags, [ - { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } - ]); - assert.equal(diags.length, 1); - - moduleFile.path = moduleFileOldPath; - host.reloadFS([moduleFile, file1]); - host.runQueuedTimeoutCallbacks(); - - // Make a change to trigger the program rebuild - const changeRequest = makeSessionRequest( - server.CommandNames.Change, - { file: file1.path, line: 1, offset: 44, endLine: 1, endOffset: 44, insertString: "\n" } - ); - session.executeCommand(changeRequest); - host.runQueuedTimeoutCallbacks(); - - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - }); - - it("should restore the states for configured projects", () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: "import * as T from './moduleFile'; T.bar();" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createServerHost([moduleFile, file1, configFile]); - const session = createSession(host); - - openFilesForSession([file1], session); - const getErrRequest = makeSessionRequest( - server.CommandNames.SemanticDiagnosticsSync, - { file: file1.path } - ); - let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - - const moduleFileOldPath = moduleFile.path; - const moduleFileNewPath = "/a/b/moduleFile1.ts"; - moduleFile.path = moduleFileNewPath; - host.reloadFS([moduleFile, file1, configFile]); - host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyDiagnostics(diags, [ - { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } - ]); - - moduleFile.path = moduleFileOldPath; - host.reloadFS([moduleFile, file1, configFile]); - host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - }); - - it("should property handle missing config files", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/b/tsconfig.json", - content: "{}" - }; - const projectName = "project1"; - const host = createServerHost([f1]); - const projectService = createProjectService(host); - projectService.openExternalProject({ rootFiles: toExternalFiles([f1.path, config.path]), options: {}, projectFileName: projectName }); - - // should have one external project since config file is missing - projectService.checkNumberOfProjects({ externalProjects: 1 }); - - host.reloadFS([f1, config]); - projectService.openExternalProject({ rootFiles: toExternalFiles([f1.path, config.path]), options: {}, projectFileName: projectName }); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - }); - - it("types should load from config file path if config exists", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } }) - }; - const node = { - path: "/a/b/node_modules/@types/node/index.d.ts", - content: "declare var process: any" - }; - const cwd = { - path: "/a/c" - }; - const host = createServerHost([f1, config, node, cwd], { currentDirectory: cwd.path }); - const projectService = createProjectService(host); - projectService.openClientFile(f1.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, node.path, config.path]); - }); - }); - - describe("tsserverProjectSystem add the missing module file for inferred project", () => { - it("should remove the `module not found` error", () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: "import * as T from './moduleFile'; T.bar();" - }; - const host = createServerHost([file1]); - const session = createSession(host); - openFilesForSession([file1], session); - const getErrRequest = makeSessionRequest( - server.CommandNames.SemanticDiagnosticsSync, - { file: file1.path } - ); - let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyDiagnostics(diags, [ - { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } - ]); - - host.reloadFS([file1, moduleFile]); - host.runQueuedTimeoutCallbacks(); - - // Make a change to trigger the program rebuild - const changeRequest = makeSessionRequest( - server.CommandNames.Change, - { file: file1.path, line: 1, offset: 44, endLine: 1, endOffset: 44, insertString: "\n" } - ); - session.executeCommand(changeRequest); - - // Recheck - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - }); - - it("npm install @types works", () => { - const folderPath = "/a/b/projects/temp"; - const file1: File = { - path: `${folderPath}/a.ts`, - content: 'import f = require("pad"); f;' - }; - const files = [file1, libFile]; - const host = createServerHost(files); - const session = createSession(host, { canUseEvents: true }); - const service = session.getProjectService(); - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { - file: file1.path, - fileContent: file1.content, - scriptKindName: "TS", - projectRootPath: folderPath - } - }); - checkNumberOfProjects(service, { inferredProjects: 1 }); - session.clearMessages(); - const expectedSequenceId = session.getNextSeq(); - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [file1.path] - } - }); - - host.checkTimeoutQueueLengthAndRun(1); - checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(); - const startOffset = file1.content.indexOf('"') + 1; - checkErrorMessage(session, "semanticDiag", { - file: file1.path, - diagnostics: [ - createDiagnostic({ line: 1, offset: startOffset }, { line: 1, offset: startOffset + '"pad"'.length }, Diagnostics.Cannot_find_module_0, ["pad"]) - ], - }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - checkErrorMessage(session, "suggestionDiag", { file: file1.path, diagnostics: [] }); - checkCompleteEvent(session, 2, expectedSequenceId); - session.clearMessages(); - - const padIndex: File = { - path: `${folderPath}/node_modules/@types/pad/index.d.ts`, - content: "export = pad;declare function pad(length: number, text: string, char ?: string): string;" - }; - files.push(padIndex); - host.reloadFS(files, { ignoreWatchInvokedWithTriggerAsFileCreate: true }); - host.runQueuedTimeoutCallbacks(); - checkProjectUpdatedInBackgroundEvent(session, [file1.path]); - session.clearMessages(); - - host.runQueuedTimeoutCallbacks(); - checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(); - checkErrorMessage(session, "semanticDiag", { file: file1.path, diagnostics: [] }); - }); - - it("suggestion diagnostics", () => { - const file: File = { - path: "/a.js", - content: "function f(p) {}", - }; - - const host = createServerHost([file]); - const session = createSession(host, { canUseEvents: true }); - const service = session.getProjectService(); - - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { file: file.path, fileContent: file.content }, - }); - - checkNumberOfProjects(service, { inferredProjects: 1 }); - session.clearMessages(); - const expectedSequenceId = session.getNextSeq(); - host.checkTimeoutQueueLengthAndRun(2); - - checkProjectUpdatedInBackgroundEvent(session, [file.path]); - session.clearMessages(); - - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [file.path], - } - }); - - host.checkTimeoutQueueLengthAndRun(1); - - checkErrorMessage(session, "syntaxDiag", { file: file.path, diagnostics: [] }, /*isMostRecent*/ true); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - - checkErrorMessage(session, "semanticDiag", { file: file.path, diagnostics: [] }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - - checkErrorMessage(session, "suggestionDiag", { - file: file.path, - diagnostics: [ - createDiagnostic({ line: 1, offset: 12 }, { line: 1, offset: 13 }, Diagnostics._0_is_declared_but_its_value_is_never_read, ["p"], "suggestion", /*reportsUnnecessary*/ true), - ], - }); - checkCompleteEvent(session, 2, expectedSequenceId); - session.clearMessages(); - }); - - it("disable suggestion diagnostics", () => { - const file: File = { - path: "/a.js", - content: 'require("b")', - }; - - const host = createServerHost([file]); - const session = createSession(host, { canUseEvents: true }); - const service = session.getProjectService(); - - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { file: file.path, fileContent: file.content }, - }); - - session.executeCommandSeq({ - command: server.CommandNames.Configure, - arguments: { - preferences: { disableSuggestions: true } - }, - }); - - checkNumberOfProjects(service, { inferredProjects: 1 }); - session.clearMessages(); - const expectedSequenceId = session.getNextSeq(); - host.checkTimeoutQueueLengthAndRun(2); - - checkProjectUpdatedInBackgroundEvent(session, [file.path]); - session.clearMessages(); - - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [file.path], - } - }); - - host.checkTimeoutQueueLengthAndRun(1); - - checkErrorMessage(session, "syntaxDiag", { file: file.path, diagnostics: [] }, /*isMostRecent*/ true); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - - checkErrorMessage(session, "semanticDiag", { file: file.path, diagnostics: [] }); - // No suggestion event, we're done. - checkCompleteEvent(session, 2, expectedSequenceId); - session.clearMessages(); - }); - - it("suppressed diagnostic events", () => { - const file: File = { - path: "/a.ts", - content: "1 = 2;", - }; - - const host = createServerHost([file]); - const session = createSession(host, { canUseEvents: true, suppressDiagnosticEvents: true }); - const service = session.getProjectService(); - - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { file: file.path, fileContent: file.content }, - }); - - checkNumberOfProjects(service, { inferredProjects: 1 }); - - host.checkTimeoutQueueLength(0); - checkNoDiagnosticEvents(session); - - session.clearMessages(); - - let expectedSequenceId = session.getNextSeq(); - - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [file.path], - } - }); - - host.checkTimeoutQueueLength(0); - checkNoDiagnosticEvents(session); - - checkCompleteEvent(session, 1, expectedSequenceId); - - session.clearMessages(); - - expectedSequenceId = session.getNextSeq(); - - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - file: file.path, - } - }); - - host.checkTimeoutQueueLength(0); - checkNoDiagnosticEvents(session); - - checkCompleteEvent(session, 1, expectedSequenceId); - - session.clearMessages(); - }); - }); - - describe("tsserverProjectSystem Configure file diagnostics events", () => { - function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string): ConfigFileDiagnostic { - const d = Diagnostics.Unknown_compiler_option_0; - const start = configFile.content.indexOf(prop) - 1; // start at "prop" - return { - fileName: configFile.path, - start, - length: prop.length + 2, - messageText: formatStringFromArgs(d.message, [prop]), - category: d.category, - code: d.code, - reportsUnnecessary: undefined - }; - } - - function getFileNotFoundDiagnostic(configFile: File, relativeFileName: string): ConfigFileDiagnostic { - const findString = `{"path":"./${relativeFileName}"}`; - const d = Diagnostics.File_0_not_found; - const start = configFile.content.indexOf(findString); - return { - fileName: configFile.path, - start, - length: findString.length, - messageText: formatStringFromArgs(d.message, [`${getDirectoryPath(configFile.path)}/${relativeFileName}`]), - category: d.category, - code: d.code, - reportsUnnecessary: undefined - }; - } - - it("are generated when the config file has errors", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "foo": "bar", - "allowJS": true - } - }` - }; - const serverEventManager = new TestServerEventManager([file, libFile, configFile]); - openFilesForSession([file], serverEventManager.session); - serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ - getUnknownCompilerOptionDiagnostic(configFile, "foo"), - getUnknownCompilerOptionDiagnostic(configFile, "allowJS") - ]); - }); - - it("are generated when the config file doesn't have errors", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {} - }` - }; - const serverEventManager = new TestServerEventManager([file, libFile, configFile]); - openFilesForSession([file], serverEventManager.session); - serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray); - }); - - it("are generated when the config file changes", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {} - }` - }; - - const files = [file, libFile, configFile]; - const serverEventManager = new TestServerEventManager(files); - openFilesForSession([file], serverEventManager.session); - serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray); - - configFile.content = `{ - "compilerOptions": { - "haha": 123 - } - }`; - serverEventManager.host.reloadFS(files); - serverEventManager.host.runQueuedTimeoutCallbacks(); - serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, [ - getUnknownCompilerOptionDiagnostic(configFile, "haha") - ]); - - configFile.content = `{ - "compilerOptions": {} - }`; - serverEventManager.host.reloadFS(files); - serverEventManager.host.runQueuedTimeoutCallbacks(); - serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, emptyArray); - }); - - it("are not generated when the config file does not include file opened and config file has errors", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const file2: File = { - path: "/a/b/test.ts", - content: "let x = 10" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "foo": "bar", - "allowJS": true - }, - "files": ["app.ts"] - }` - }; - const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]); - openFilesForSession([file2], serverEventManager.session); - serverEventManager.hasZeroEvent("configFileDiag"); - }); - - it("are not generated when the config file has errors but suppressDiagnosticEvents is true", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "foo": "bar", - "allowJS": true - } - }` - }; - const serverEventManager = new TestServerEventManager([file, libFile, configFile], /*suppressDiagnosticEvents*/ true); - openFilesForSession([file], serverEventManager.session); - serverEventManager.hasZeroEvent("configFileDiag"); - }); - - it("are not generated when the config file does not include file opened and doesnt contain any errors", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const file2: File = { - path: "/a/b/test.ts", - content: "let x = 10" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ - "files": ["app.ts"] - }` - }; - - const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]); - openFilesForSession([file2], serverEventManager.session); - serverEventManager.hasZeroEvent("configFileDiag"); - }); - - it("contains the project reference errors", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const noSuchTsconfig = "no-such-tsconfig.json"; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ - "files": ["app.ts"], - "references": [{"path":"./${noSuchTsconfig}"}] - }` - }; - - const serverEventManager = new TestServerEventManager([file, libFile, configFile]); - openFilesForSession([file], serverEventManager.session); - serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ - getFileNotFoundDiagnostic(configFile, noSuchTsconfig) - ]); - }); - }); - describe("tsserverProjectSystem skipLibCheck", () => { it("should be turned on for js-only inferred projects", () => { const file1 = { @@ -5698,70 +5079,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem Options Diagnostic locations reported correctly with changes in configFile contents", () => { - it("when options change", () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFileContentBeforeComment = `{`; - const configFileContentComment = ` - // comment`; - const configFileContentAfterComment = ` - "compilerOptions": { - "allowJs": true, - "declaration": true - } - }`; - const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment; - const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment; - - const configFile = { - path: "/a/b/tsconfig.json", - content: configFileContentWithComment - }; - const host = createServerHost([file, libFile, configFile]); - const session = createSession(host); - openFilesForSession([file], session); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const projectName = configuredProjectAt(projectService, 0).getProjectName(); - - const diags = session.executeCommand({ - type: "request", - command: server.CommandNames.SemanticDiagnosticsSync, - seq: 2, - arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } - }).response as ReadonlyArray; - assert.isTrue(diags.length === 2); - - configFile.content = configFileContentWithoutCommentLine; - host.reloadFS([file, configFile]); - - const diagsAfterEdit = session.executeCommand({ - type: "request", - command: server.CommandNames.SemanticDiagnosticsSync, - seq: 2, - arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } - }).response as ReadonlyArray; - assert.isTrue(diagsAfterEdit.length === 2); - - verifyDiagnostic(diags[0], diagsAfterEdit[0]); - verifyDiagnostic(diags[1], diagsAfterEdit[1]); - - function verifyDiagnostic(beforeEditDiag: server.protocol.DiagnosticWithLinePosition, afterEditDiag: server.protocol.DiagnosticWithLinePosition) { - assert.equal(beforeEditDiag.message, afterEditDiag.message); - assert.equal(beforeEditDiag.code, afterEditDiag.code); - assert.equal(beforeEditDiag.category, afterEditDiag.category); - assert.equal(beforeEditDiag.startLocation.line, afterEditDiag.startLocation.line + 1); - assert.equal(beforeEditDiag.startLocation.offset, afterEditDiag.startLocation.offset); - assert.equal(beforeEditDiag.endLocation.line, afterEditDiag.endLocation.line + 1); - assert.equal(beforeEditDiag.endLocation.offset, afterEditDiag.endLocation.offset); - } - }); - }); - describe("tsserverProjectSystem refactors", () => { it("use formatting options", () => { const file = { @@ -8735,35 +8052,6 @@ fn5(); }); }); - describe("tsserverProjectSystem config file change", () => { - it("Updates diagnostics when '--noUnusedLabels' changes", () => { - const aTs: File = { path: "/a.ts", content: "label: while (1) {}" }; - const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`; - const tsconfig: File = { path: "/tsconfig.json", content: options(/*allowUnusedLabels*/ true) }; - - const host = createServerHost([aTs, tsconfig]); - const session = createSession(host); - openFilesForSession([aTs], session); - - host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ false)); - host.runQueuedTimeoutCallbacks(); - - const response = executeSessionRequest(session, protocol.CommandTypes.SemanticDiagnosticsSync, { file: aTs.path }) as protocol.Diagnostic[] | undefined; - assert.deepEqual(response, [ - { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 + "label".length }, - text: "Unused label.", - category: "error", - code: Diagnostics.Unused_label.code, - relatedInformation: undefined, - reportsUnnecessary: true, - source: undefined, - }, - ]); - }); - }); - describe("tsserverProjectSystem with metadata in response", () => { const metadata = "Extra Info"; function verifyOutput(host: TestServerHost, expectedResponse: protocol.Response) { From 160c73f9420bcc9dfa556d2aef70850ec809f19a Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 13:12:14 -0800 Subject: [PATCH 13/20] Separate out caching file system test --- src/testRunner/tsconfig.json | 1 + .../tsserverCachingFileSystemInformation.ts | 704 +++++++++++++++++ src/testRunner/unittests/tsserverHelpers.ts | 10 +- .../unittests/tsserverProjectSystem.ts | 707 ------------------ 4 files changed, 708 insertions(+), 714 deletions(-) create mode 100644 src/testRunner/unittests/tsserverCachingFileSystemInformation.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index ba46706ee16..daad5a29b6a 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -88,6 +88,7 @@ "unittests/tsconfigParsing.ts", "unittests/tscWatchEmit.ts", "unittests/tscWatchMode.ts", + "unittests/tsserverCachingFileSystemInformation.ts", "unittests/tsserverLargeFileReferencedEvent.ts", "unittests/tsserverProjectLoadingEvents.ts", "unittests/tsserverProjectSystem.ts", diff --git a/src/testRunner/unittests/tsserverCachingFileSystemInformation.ts b/src/testRunner/unittests/tsserverCachingFileSystemInformation.ts new file mode 100644 index 00000000000..65e1ec564d3 --- /dev/null +++ b/src/testRunner/unittests/tsserverCachingFileSystemInformation.ts @@ -0,0 +1,704 @@ +namespace ts.projectSystem { + function getNumberOfWatchesInvokedForRecursiveWatches(recursiveWatchedDirs: string[], file: string) { + return countWhere(recursiveWatchedDirs, dir => file.length > dir.length && startsWith(file, dir) && file[dir.length] === directorySeparator); + } + + describe("tsserverCachingFileSystemInformation:: tsserverProjectSystem CachingFileSystemInformation", () => { + enum CalledMapsWithSingleArg { + fileExists = "fileExists", + directoryExists = "directoryExists", + getDirectories = "getDirectories", + readFile = "readFile" + } + enum CalledMapsWithFiveArgs { + readDirectory = "readDirectory" + } + type CalledMaps = CalledMapsWithSingleArg | CalledMapsWithFiveArgs; + function createCallsTrackingHost(host: TestServerHost) { + const calledMaps: Record> & Record, ReadonlyArray, ReadonlyArray, number]>> = { + fileExists: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.fileExists), + directoryExists: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.directoryExists), + getDirectories: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.getDirectories), + readFile: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.readFile), + readDirectory: setCallsTrackingWithFiveArgFn(CalledMapsWithFiveArgs.readDirectory) + }; + + return { + verifyNoCall, + verifyCalledOnEachEntryNTimes, + verifyCalledOnEachEntry, + verifyNoHostCalls, + verifyNoHostCallsExceptFileExistsOnce, + verifyCalledOn, + clear + }; + + function setCallsTrackingWithSingleArgFn(prop: CalledMapsWithSingleArg) { + const calledMap = createMultiMap(); + const cb = (host)[prop].bind(host); + (host)[prop] = (f: string) => { + calledMap.add(f, /*value*/ true); + return cb(f); + }; + return calledMap; + } + + function setCallsTrackingWithFiveArgFn(prop: CalledMapsWithFiveArgs) { + const calledMap = createMultiMap<[U, V, W, X]>(); + const cb = (host)[prop].bind(host); + (host)[prop] = (f: string, arg1?: U, arg2?: V, arg3?: W, arg4?: X) => { + calledMap.add(f, [arg1!, arg2!, arg3!, arg4!]); // TODO: GH#18217 + return cb(f, arg1, arg2, arg3, arg4); + }; + return calledMap; + } + + function verifyCalledOn(callback: CalledMaps, name: string) { + const calledMap = calledMaps[callback]; + const result = calledMap.get(name); + assert.isTrue(result && !!result.length, `${callback} should be called with name: ${name}: ${arrayFrom(calledMap.keys())}`); + } + + function verifyNoCall(callback: CalledMaps) { + const calledMap = calledMaps[callback]; + assert.equal(calledMap.size, 0, `${callback} shouldn't be called: ${arrayFrom(calledMap.keys())}`); + } + + function verifyCalledOnEachEntry(callback: CalledMaps, expectedKeys: Map) { + TestFSWithWatch.checkMultiMapKeyCount(callback, calledMaps[callback], expectedKeys); + } + + function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: ReadonlyArray, nTimes: number) { + TestFSWithWatch.checkMultiMapKeyCount(callback, calledMaps[callback], expectedKeys, nTimes); + } + + function verifyNoHostCalls() { + iterateOnCalledMaps(key => verifyNoCall(key)); + } + + function verifyNoHostCallsExceptFileExistsOnce(expectedKeys: ReadonlyArray) { + verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, expectedKeys, 1); + verifyNoCall(CalledMapsWithSingleArg.directoryExists); + verifyNoCall(CalledMapsWithSingleArg.getDirectories); + verifyNoCall(CalledMapsWithSingleArg.readFile); + verifyNoCall(CalledMapsWithFiveArgs.readDirectory); + } + + function clear() { + iterateOnCalledMaps(key => calledMaps[key].clear()); + } + + function iterateOnCalledMaps(cb: (key: CalledMaps) => void) { + for (const key in CalledMapsWithSingleArg) { + cb(key as CalledMapsWithSingleArg); + } + for (const key in CalledMapsWithFiveArgs) { + cb(key as CalledMapsWithFiveArgs); + } + } + } + + it("works using legacy resolution logic", () => { + let rootContent = `import {x} from "f1"`; + const root: File = { + path: "/c/d/f0.ts", + content: rootContent + }; + + const imported: File = { + path: "/c/f1.ts", + content: `foo()` + }; + + const host = createServerHost([root, imported]); + const projectService = createProjectService(host); + projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true }); + projectService.openClientFile(root.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + const rootScriptInfo = project.getRootScriptInfos()[0]; + assert.equal(rootScriptInfo.fileName, root.path); + + // ensure that imported file was found + verifyImportedDiagnostics(); + + const callsTrackingHost = createCallsTrackingHost(host); + + // trigger synchronization to make sure that import will be fetched from the cache + // ensure file has correct number of errors after edit + editContent(`import {x} from "f1"; + var x: string = 1;`); + verifyImportedDiagnostics(); + callsTrackingHost.verifyNoHostCalls(); + + // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + editContent(`import {x} from "f2"`); + try { + // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + verifyImportedDiagnostics(); + assert.isTrue(false, `should not find file '${imported.path}'`); + } + catch (e) { + assert.isTrue(e.message.indexOf(`Could not find file: '${imported.path}'.`) === 0); + } + const f2Lookups = getLocationsForModuleLookup("f2"); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, f2Lookups, 1); + const f2DirLookups = getLocationsForDirectoryLookup(); + callsTrackingHost.verifyCalledOnEachEntry(CalledMapsWithSingleArg.directoryExists, f2DirLookups); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.readFile); + callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); + + editContent(`import {x} from "f1"`); + verifyImportedDiagnostics(); + const f1Lookups = f2Lookups.map(s => s.replace("f2", "f1")); + f1Lookups.length = f1Lookups.indexOf(imported.path) + 1; + const f1DirLookups = ["/c/d", "/c", ...mapCombinedPathsInAncestor(getDirectoryPath(root.path), nodeModulesAtTypes, returnTrue)]; + vertifyF1Lookups(); + + // setting compiler options discards module resolution cache + callsTrackingHost.clear(); + projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true, target: ScriptTarget.ES5 }); + verifyImportedDiagnostics(); + vertifyF1Lookups(); + + function vertifyF1Lookups() { + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, f1Lookups, 1); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.directoryExists, f1DirLookups, 1); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.readFile); + callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); + } + + function editContent(newContent: string) { + callsTrackingHost.clear(); + rootScriptInfo.editContent(0, rootContent.length, newContent); + rootContent = newContent; + } + + function verifyImportedDiagnostics() { + const diags = project.getLanguageService().getSemanticDiagnostics(imported.path); + assert.equal(diags.length, 1); + const diag = diags[0]; + assert.equal(diag.code, Diagnostics.Cannot_find_name_0.code); + assert.equal(flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find name 'foo'."); + } + + function getLocationsForModuleLookup(module: string) { + const locations: string[] = []; + forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { + locations.push( + combinePaths(ancestor, `${module}.ts`), + combinePaths(ancestor, `${module}.tsx`), + combinePaths(ancestor, `${module}.d.ts`) + ); + }); + forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { + locations.push( + combinePaths(ancestor, `${module}.js`), + combinePaths(ancestor, `${module}.jsx`) + ); + }); + return locations; + } + + function getLocationsForDirectoryLookup() { + const result = createMap(); + forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { + // To resolve modules + result.set(ancestor, 2); + // for type roots + result.set(combinePaths(ancestor, nodeModules), 1); + result.set(combinePaths(ancestor, nodeModulesAtTypes), 1); + }); + return result; + } + }); + + it("loads missing files from disk", () => { + const root: File = { + path: "/c/foo.ts", + content: `import {y} from "bar"` + }; + + const imported: File = { + path: "/c/bar.d.ts", + content: `export var y = 1` + }; + + const host = createServerHost([root]); + const projectService = createProjectService(host); + projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true }); + const callsTrackingHost = createCallsTrackingHost(host); + projectService.openClientFile(root.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + const rootScriptInfo = project.getRootScriptInfos()[0]; + assert.equal(rootScriptInfo.fileName, root.path); + + let diags = project.getLanguageService().getSemanticDiagnostics(root.path); + assert.equal(diags.length, 1); + const diag = diags[0]; + assert.equal(diag.code, Diagnostics.Cannot_find_module_0.code); + assert.equal(flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find module 'bar'."); + callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); + + + callsTrackingHost.clear(); + host.reloadFS([root, imported]); + host.runQueuedTimeoutCallbacks(); + diags = project.getLanguageService().getSemanticDiagnostics(root.path); + assert.equal(diags.length, 0); + callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); + }); + + it("when calling goto definition of module", () => { + const clientFile: File = { + path: "/a/b/controllers/vessels/client.ts", + content: ` + import { Vessel } from '~/models/vessel'; + const v = new Vessel(); + ` + }; + const anotherModuleFile: File = { + path: "/a/b/utils/db.ts", + content: "export class Bookshelf { }" + }; + const moduleFile: File = { + path: "/a/b/models/vessel.ts", + content: ` + import { Bookshelf } from '~/utils/db'; + export class Vessel extends Bookshelf {} + ` + }; + const tsconfigFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + target: "es6", + module: "es6", + baseUrl: "./", // all paths are relative to the baseUrl + paths: { + "~/*": ["*"] // resolve any `~/foo/bar` to `/foo/bar` + } + }, + exclude: [ + "api", + "build", + "node_modules", + "public", + "seeds", + "sql_updates", + "tests.build" + ] + }) + }; + const projectFiles = [clientFile, anotherModuleFile, moduleFile, tsconfigFile]; + const host = createServerHost(projectFiles); + const session = createSession(host); + const projectService = session.getProjectService(); + const { configFileName } = projectService.openClientFile(clientFile.path); + + assert.isDefined(configFileName, `should find config`); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = projectService.configuredProjects.get(tsconfigFile.path)!; + checkProjectActualFiles(project, map(projectFiles, f => f.path)); + + const callsTrackingHost = createCallsTrackingHost(host); + + // Get definitions shouldnt make host requests + const getDefinitionRequest = makeSessionRequest(protocol.CommandTypes.Definition, { + file: clientFile.path, + position: clientFile.content.indexOf("/vessel") + 1, + line: undefined!, // TODO: GH#18217 + offset: undefined! // TODO: GH#18217 + }); + 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(); + + // Open the file should call only file exists on module directory and use cached value for parental directory + const { configFileName: config2 } = projectService.openClientFile(moduleFile.path); + assert.equal(config2, configFileName); + callsTrackingHost.verifyNoHostCallsExceptFileExistsOnce(["/a/b/models/tsconfig.json", "/a/b/models/jsconfig.json"]); + + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(tsconfigFile.path), project); + }); + + describe("WatchDirectories for config file with", () => { + function verifyWatchDirectoriesCaseSensitivity(useCaseSensitiveFileNames: boolean) { + const frontendDir = "/Users/someuser/work/applications/frontend"; + const toCanonical: (s: string) => Path = useCaseSensitiveFileNames ? s => s as Path : s => s.toLowerCase() as Path; + const canonicalFrontendDir = toCanonical(frontendDir); + const file1: File = { + path: `${frontendDir}/src/app/utils/Analytic.ts`, + content: "export class SomeClass { };" + }; + const file2: File = { + path: `${frontendDir}/src/app/redux/configureStore.ts`, + content: "export class configureStore { }" + }; + const file3: File = { + path: `${frontendDir}/src/app/utils/Cookie.ts`, + content: "export class Cookie { }" + }; + const es2016LibFile: File = { + path: "/a/lib/lib.es2016.full.d.ts", + content: libFile.content + }; + const typeRoots = ["types", "node_modules/@types"]; + const types = ["node", "jest"]; + const tsconfigFile: File = { + path: `${frontendDir}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + strict: true, + strictNullChecks: true, + target: "es2016", + module: "commonjs", + moduleResolution: "node", + sourceMap: true, + noEmitOnError: true, + experimentalDecorators: true, + emitDecoratorMetadata: true, + types, + noUnusedLocals: true, + outDir: "./compiled", + typeRoots, + baseUrl: ".", + paths: { + "*": [ + "types/*" + ] + } + }, + include: [ + "src/**/*" + ], + exclude: [ + "node_modules", + "compiled" + ] + }) + }; + const projectFiles = [file1, file2, es2016LibFile, tsconfigFile]; + const host = createServerHost(projectFiles, { useCaseSensitiveFileNames }); + const projectService = createProjectService(host); + const canonicalConfigPath = toCanonical(tsconfigFile.path); + const { configFileName } = projectService.openClientFile(file1.path); + assert.equal(configFileName, tsconfigFile.path as server.NormalizedPath, `should find config`); // tslint:disable-line no-unnecessary-type-assertion (TODO: GH#18217) + checkNumberOfConfiguredProjects(projectService, 1); + const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, `${canonicalFrontendDir}/types`, `${canonicalFrontendDir}/node_modules`].concat(getNodeModuleDirectories(getDirectoryPath(canonicalFrontendDir))); + + const project = projectService.configuredProjects.get(canonicalConfigPath)!; + verifyProjectAndWatchedDirectories(); + + const callsTrackingHost = createCallsTrackingHost(host); + + // Create file cookie.ts + projectFiles.push(file3); + host.reloadFS(projectFiles); + host.runQueuedTimeoutCallbacks(); + + const canonicalFile3Path = useCaseSensitiveFileNames ? file3.path : file3.path.toLocaleLowerCase(); + const numberOfTimesWatchInvoked = getNumberOfWatchesInvokedForRecursiveWatches(watchingRecursiveDirectories, canonicalFile3Path); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, [canonicalFile3Path], numberOfTimesWatchInvoked); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.directoryExists, [canonicalFile3Path], numberOfTimesWatchInvoked); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.readFile, [file3.path], 1); + callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); + + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); + verifyProjectAndWatchedDirectories(); + + callsTrackingHost.clear(); + + const { configFileName: configFile2 } = projectService.openClientFile(file3.path); + assert.equal(configFile2, configFileName); + + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); + verifyProjectAndWatchedDirectories(); + callsTrackingHost.verifyNoHostCalls(); + + function getFilePathIfNotOpen(f: File) { + const path = toCanonical(f.path); + const info = projectService.getScriptInfoForPath(toCanonical(f.path)); + return info && info.isScriptOpen() ? undefined : path; + } + + function verifyProjectAndWatchedDirectories() { + checkProjectActualFiles(project, map(projectFiles, f => f.path)); + checkWatchedFiles(host, mapDefined(projectFiles, getFilePathIfNotOpen)); + checkWatchedDirectories(host, watchingRecursiveDirectories, /*recursive*/ true); + checkWatchedDirectories(host, [], /*recursive*/ false); + } + } + + it("case insensitive file system", () => { + verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ false); + }); + + it("case sensitive file system", () => { + verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ true); + }); + }); + + describe("Subfolder invalidations correctly include parent folder failed lookup locations", () => { + function runFailedLookupTest(resolution: "Node" | "Classic") { + const projectLocation = "/proj"; + const file1: File = { + path: `${projectLocation}/foo/boo/app.ts`, + content: `import * as debug from "debug"` + }; + const file2: File = { + path: `${projectLocation}/foo/boo/moo/app.ts`, + content: `import * as debug from "debug"` + }; + const tsconfig: File = { + path: `${projectLocation}/tsconfig.json`, + content: JSON.stringify({ + files: ["foo/boo/app.ts", "foo/boo/moo/app.ts"], + moduleResolution: resolution + }) + }; + + const files = [file1, file2, tsconfig, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(file1.path); + + const project = service.configuredProjects.get(tsconfig.path)!; + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); + + const debugTypesFile: File = { + path: `${projectLocation}/node_modules/debug/index.d.ts`, + content: "export {}" + }; + files.push(debugTypesFile); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), []); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), []); + } + + it("Includes the parent folder FLLs in node module resolution mode", () => { + runFailedLookupTest("Node"); + }); + it("Includes the parent folder FLLs in classic module resolution mode", () => { + runFailedLookupTest("Classic"); + }); + }); + + describe("Verify npm install in directory with tsconfig file works when", () => { + function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { + const root = "/user/username/rootfolder/otherfolder"; + const getRootedFileOrFolder = (fileOrFolder: File) => { + fileOrFolder.path = root + fileOrFolder.path; + return fileOrFolder; + }; + const app: File = getRootedFileOrFolder({ + path: "/a/b/app.ts", + content: "import _ from 'lodash';" + }); + const tsconfigJson: File = getRootedFileOrFolder({ + path: "/a/b/tsconfig.json", + content: '{ "compilerOptions": { } }' + }); + const packageJson: File = getRootedFileOrFolder({ + path: "/a/b/package.json", + content: ` +{ + "name": "test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": { + "lodash", + "rxjs" + }, + "devDependencies": { + "@types/lodash", + "typescript" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} +` + }); + const appFolder = getDirectoryPath(app.path); + const projectFiles = [app, libFile, tsconfigJson]; + const typeRootDirectories = getTypeRootsFromLocation(getDirectoryPath(tsconfigJson.path)); + const otherFiles = [packageJson]; + const host = createServerHost(projectFiles.concat(otherFiles)); + const projectService = createProjectService(host); + const { configFileName } = projectService.openClientFile(app.path); + assert.equal(configFileName, tsconfigJson.path as server.NormalizedPath, `should find config`); // TODO: GH#18217 + const recursiveWatchedDirectories: string[] = [`${appFolder}`, `${appFolder}/node_modules`].concat(getNodeModuleDirectories(getDirectoryPath(appFolder))); + verifyProject(); + + let npmInstallComplete = false; + + // Simulate npm install + const filesAndFoldersToAdd: File[] = [ + { path: "/a/b/node_modules" }, + { path: "/a/b/node_modules/.staging/@types" }, + { path: "/a/b/node_modules/.staging/lodash-b0733faa" }, + { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61" }, + { path: "/a/b/node_modules/.staging/typescript-8493ea5d" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/package.json", content: "{\n \"name\": \"symbol-observable\",\n \"version\": \"1.0.4\",\n \"description\": \"Symbol.observable ponyfill\",\n \"license\": \"MIT\",\n \"repository\": \"blesh/symbol-observable\",\n \"author\": {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"engines\": {\n \"node\": \">=0.10.0\"\n },\n \"scripts\": {\n \"test\": \"npm run build && mocha && tsc ./ts-test/test.ts && node ./ts-test/test.js && check-es3-syntax -p lib/ --kill\",\n \"build\": \"babel es --out-dir lib\",\n \"prepublish\": \"npm test\"\n },\n \"files\": [\n \"" }, + { path: "/a/b/node_modules/.staging/lodash-b0733faa/package.json", content: "{\n \"name\": \"lodash\",\n \"version\": \"4.17.4\",\n \"description\": \"Lodash modular utilities.\",\n \"keywords\": \"modules, stdlib, util\",\n \"homepage\": \"https://lodash.com/\",\n \"repository\": \"lodash/lodash\",\n \"icon\": \"https://lodash.com/icon.svg\",\n \"license\": \"MIT\",\n \"main\": \"lodash.js\",\n \"author\": \"John-David Dalton (http://allyoucanleet.com/)\",\n \"contributors\": [\n \"John-David Dalton (http://allyoucanleet.com/)\",\n \"Mathias Bynens \",\n \"contributors\": [\n {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n {\n \"name\": \"Paul Taylor\",\n \"email\": \"paul.e.taylor@me.com\"\n },\n {\n \"name\": \"Jeff Cross\",\n \"email\": \"crossj@google.com\"\n },\n {\n \"name\": \"Matthew Podwysocki\",\n \"email\": \"matthewp@microsoft.com\"\n },\n {\n \"name\": \"OJ Kwon\",\n \"email\": \"kwon.ohjoong@gmail.com\"\n },\n {\n \"name\": \"Andre Staltz\",\n \"email\": \"andre@staltz.com\"\n }\n ],\n \"license\": \"Apache-2.0\",\n \"bugs\": {\n \"url\": \"https://github.com/ReactiveX/RxJS/issues\"\n },\n \"homepage\": \"https://github.com/ReactiveX/RxJS\",\n \"devDependencies\": {\n \"babel-polyfill\": \"^6.23.0\",\n \"benchmark\": \"^2.1.0\",\n \"benchpress\": \"2.0.0-beta.1\",\n \"chai\": \"^3.5.0\",\n \"color\": \"^0.11.1\",\n \"colors\": \"1.1.2\",\n \"commitizen\": \"^2.8.6\",\n \"coveralls\": \"^2.11.13\",\n \"cz-conventional-changelog\": \"^1.2.0\",\n \"danger\": \"^1.1.0\",\n \"doctoc\": \"^1.0.0\",\n \"escape-string-regexp\": \"^1.0.5 \",\n \"esdoc\": \"^0.4.7\",\n \"eslint\": \"^3.8.0\",\n \"fs-extra\": \"^2.1.2\",\n \"get-folder-size\": \"^1.0.0\",\n \"glob\": \"^7.0.3\",\n \"gm\": \"^1.22.0\",\n \"google-closure-compiler-js\": \"^20170218.0.0\",\n \"gzip-size\": \"^3.0.0\",\n \"http-server\": \"^0.9.0\",\n \"husky\": \"^0.13.3\",\n \"lint-staged\": \"3.2.5\",\n \"lodash\": \"^4.15.0\",\n \"madge\": \"^1.4.3\",\n \"markdown-doctest\": \"^0.9.1\",\n \"minimist\": \"^1.2.0\",\n \"mkdirp\": \"^0.5.1\",\n \"mocha\": \"^3.0.2\",\n \"mocha-in-sauce\": \"0.0.1\",\n \"npm-run-all\": \"^4.0.2\",\n \"npm-scripts-info\": \"^0.3.4\",\n \"nyc\": \"^10.2.0\",\n \"opn-cli\": \"^3.1.0\",\n \"platform\": \"^1.3.1\",\n \"promise\": \"^7.1.1\",\n \"protractor\": \"^3.1.1\",\n \"rollup\": \"0.36.3\",\n \"rollup-plugin-inject\": \"^2.0.0\",\n \"rollup-plugin-node-resolve\": \"^2.0.0\",\n \"rx\": \"latest\",\n \"rxjs\": \"latest\",\n \"shx\": \"^0.2.2\",\n \"sinon\": \"^2.1.0\",\n \"sinon-chai\": \"^2.9.0\",\n \"source-map-support\": \"^0.4.0\",\n \"tslib\": \"^1.5.0\",\n \"tslint\": \"^4.4.2\",\n \"typescript\": \"~2.0.6\",\n \"typings\": \"^2.0.0\",\n \"validate-commit-msg\": \"^2.14.0\",\n \"watch\": \"^1.0.1\",\n \"webpack\": \"^1.13.1\",\n \"xmlhttprequest\": \"1.8.0\"\n },\n \"engines\": {\n \"npm\": \">=2.0.0\"\n },\n \"typings\": \"Rx.d.ts\",\n \"dependencies\": {\n \"symbol-observable\": \"^1.0.1\"\n }\n}" }, + { path: "/a/b/node_modules/.staging/typescript-8493ea5d/package.json", content: "{\n \"name\": \"typescript\",\n \"author\": \"Microsoft Corp.\",\n \"homepage\": \"http://typescriptlang.org/\",\n \"version\": \"2.4.2\",\n \"license\": \"Apache-2.0\",\n \"description\": \"TypeScript is a language for application scale JavaScript development\",\n \"keywords\": [\n \"TypeScript\",\n \"Microsoft\",\n \"compiler\",\n \"language\",\n \"javascript\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/Microsoft/TypeScript/issues\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/Microsoft/TypeScript.git\"\n },\n \"main\": \"./lib/typescript.js\",\n \"typings\": \"./lib/typescript.d.ts\",\n \"bin\": {\n \"tsc\": \"./bin/tsc\",\n \"tsserver\": \"./bin/tsserver\"\n },\n \"engines\": {\n \"node\": \">=4.2.0\"\n },\n \"devDependencies\": {\n \"@types/browserify\": \"latest\",\n \"@types/chai\": \"latest\",\n \"@types/convert-source-map\": \"latest\",\n \"@types/del\": \"latest\",\n \"@types/glob\": \"latest\",\n \"@types/gulp\": \"latest\",\n \"@types/gulp-concat\": \"latest\",\n \"@types/gulp-help\": \"latest\",\n \"@types/gulp-newer\": \"latest\",\n \"@types/gulp-sourcemaps\": \"latest\",\n \"@types/merge2\": \"latest\",\n \"@types/minimatch\": \"latest\",\n \"@types/minimist\": \"latest\",\n \"@types/mkdirp\": \"latest\",\n \"@types/mocha\": \"latest\",\n \"@types/node\": \"latest\",\n \"@types/q\": \"latest\",\n \"@types/run-sequence\": \"latest\",\n \"@types/through2\": \"latest\",\n \"browserify\": \"latest\",\n \"chai\": \"latest\",\n \"convert-source-map\": \"latest\",\n \"del\": \"latest\",\n \"gulp\": \"latest\",\n \"gulp-clone\": \"latest\",\n \"gulp-concat\": \"latest\",\n \"gulp-help\": \"latest\",\n \"gulp-insert\": \"latest\",\n \"gulp-newer\": \"latest\",\n \"gulp-sourcemaps\": \"latest\",\n \"gulp-typescript\": \"latest\",\n \"into-stream\": \"latest\",\n \"istanbul\": \"latest\",\n \"jake\": \"latest\",\n \"merge2\": \"latest\",\n \"minimist\": \"latest\",\n \"mkdirp\": \"latest\",\n \"mocha\": \"latest\",\n \"mocha-fivemat-progress-reporter\": \"latest\",\n \"q\": \"latest\",\n \"run-sequence\": \"latest\",\n \"sorcery\": \"latest\",\n \"through2\": \"latest\",\n \"travis-fold\": \"latest\",\n \"ts-node\": \"latest\",\n \"tslint\": \"latest\",\n \"typescript\": \"^2.4\"\n },\n \"scripts\": {\n \"pretest\": \"jake tests\",\n \"test\": \"jake runtests-parallel\",\n \"build\": \"npm run build:compiler && npm run build:tests\",\n \"build:compiler\": \"jake local\",\n \"build:tests\": \"jake tests\",\n \"start\": \"node lib/tsc\",\n \"clean\": \"jake clean\",\n \"gulp\": \"gulp\",\n \"jake\": \"jake\",\n \"lint\": \"jake lint\",\n \"setup-hooks\": \"node scripts/link-hooks.js\"\n },\n \"browser\": {\n \"buffer\": false,\n \"fs\": false,\n \"os\": false,\n \"path\": false\n }\n}" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/index.js", content: "module.exports = require('./lib/index');\n" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/index.d.ts", content: "declare const observableSymbol: symbol;\nexport default observableSymbol;\n" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/lib" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/lib/index.js", content: "'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _ponyfill = require('./ponyfill');\n\nvar _ponyfill2 = _interopRequireDefault(_ponyfill);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\nvar root; /* global window */\n\n\nif (typeof self !== 'undefined') {\n root = self;\n} else if (typeof window !== 'undefined') {\n root = window;\n} else if (typeof global !== 'undefined') {\n root = global;\n} else if (typeof module !== 'undefined') {\n root = module;\n} else {\n root = Function('return this')();\n}\n\nvar result = (0, _ponyfill2['default'])(root);\nexports['default'] = result;" }, + ].map(getRootedFileOrFolder); + verifyAfterPartialOrCompleteNpmInstall(2); + + filesAndFoldersToAdd.push(...[ + { path: "/a/b/node_modules/.staging/typescript-8493ea5d/lib" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/add/operator" }, + { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/package.json", content: "{\n \"name\": \"@types/lodash\",\n \"version\": \"4.14.74\",\n \"description\": \"TypeScript definitions for Lo-Dash\",\n \"license\": \"MIT\",\n \"contributors\": [\n {\n \"name\": \"Brian Zengel\",\n \"url\": \"https://github.com/bczengel\"\n },\n {\n \"name\": \"Ilya Mochalov\",\n \"url\": \"https://github.com/chrootsu\"\n },\n {\n \"name\": \"Stepan Mikhaylyuk\",\n \"url\": \"https://github.com/stepancar\"\n },\n {\n \"name\": \"Eric L Anderson\",\n \"url\": \"https://github.com/ericanderson\"\n },\n {\n \"name\": \"AJ Richardson\",\n \"url\": \"https://github.com/aj-r\"\n },\n {\n \"name\": \"Junyoung Clare Jang\",\n \"url\": \"https://github.com/ailrun\"\n }\n ],\n \"main\": \"\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://www.github.com/DefinitelyTyped/DefinitelyTyped.git\"\n },\n \"scripts\": {},\n \"dependencies\": {},\n \"typesPublisherContentHash\": \"12af578ffaf8d86d2df37e591857906a86b983fa9258414326544a0fe6af0de8\",\n \"typeScriptVersion\": \"2.2\"\n}" }, + { path: "/a/b/node_modules/.staging/lodash-b0733faa/index.js", content: "module.exports = require('./lodash');" }, + { path: "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" } + ].map(getRootedFileOrFolder)); + // Since we added/removed in .staging no timeout + verifyAfterPartialOrCompleteNpmInstall(0); + + // Remove file "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" + filesAndFoldersToAdd.length--; + verifyAfterPartialOrCompleteNpmInstall(0); + + filesAndFoldersToAdd.push(...[ + { path: "/a/b/node_modules/.staging/rxjs-22375c61/bundles" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/operator" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/src/add/observable/dom" }, + { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/index.d.ts", content: "\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(0); + + 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(...[ + { path: "/a/b/node_modules/symbol-observable" }, + { path: "/a/b/node_modules/@types" }, + { path: "/a/b/node_modules/@types/lodash" }, + { path: "/a/b/node_modules/lodash" }, + { path: "/a/b/node_modules/rxjs" }, + { path: "/a/b/node_modules/typescript" }, + { path: "/a/b/node_modules/.bin" } + ].map(getRootedFileOrFolder)); + // From the type root update + verifyAfterPartialOrCompleteNpmInstall(2); + + forEach(filesAndFoldersToAdd, f => { + f.path = f.path + .replace("/a/b/node_modules/.staging", "/a/b/node_modules") + .replace(/[\-\.][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w]/g, ""); + }); + + const lodashIndexPath = 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 = 2; + // npm installation complete, timeout after reload fs + npmInstallComplete = true; + verifyAfterPartialOrCompleteNpmInstall(2); + + function verifyAfterPartialOrCompleteNpmInstall(timeoutQueueLengthWhenRunningTimeouts: number) { + host.reloadFS(projectFiles.concat(otherFiles, filesAndFoldersToAdd)); + if (npmInstallComplete || timeoutDuringPartialInstallation) { + host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts); + } + else { + host.checkTimeoutQueueLength(2); + } + verifyProject(); + } + + function verifyProject() { + checkNumberOfConfiguredProjects(projectService, 1); + + const project = projectService.configuredProjects.get(tsconfigJson.path)!; + const projectFilePaths = map(projectFiles, f => f.path); + checkProjectActualFiles(project, projectFilePaths); + + const filesWatched = filter(projectFilePaths, p => p !== app.path && p.indexOf("/a/b/node_modules") === -1); + checkWatchedFiles(host, filesWatched); + checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true); + checkWatchedDirectories(host, [], /*recursive*/ false); + } + } + + it("timeouts occur inbetween installation", () => { + verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); + }); + + it("timeout occurs after installation", () => { + verifyNpmInstall(/*timeoutDuringPartialInstallation*/ false); + }); + }); + + it("when node_modules dont receive event for the @types file addition", () => { + const projectLocation = "/user/username/folder/myproject"; + const app: File = { + path: `${projectLocation}/app.ts`, + content: `import * as debug from "debug"` + }; + const tsconfig: File = { + path: `${projectLocation}/tsconfig.json`, + content: "" + }; + + const files = [app, tsconfig, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(app.path); + + const project = service.configuredProjects.get(tsconfig.path)!; + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); + + const debugTypesFile: File = { + path: `${projectLocation}/node_modules/@types/debug/index.d.ts`, + content: "export {}" + }; + files.push(debugTypesFile); + // Do not invoke recursive directory watcher for anything other than node_module/@types + const invoker = host.invokeWatchedDirectoriesRecursiveCallback; + host.invokeWatchedDirectoriesRecursiveCallback = (fullPath, relativePath) => { + if (fullPath.endsWith("@types")) { + invoker.call(host, fullPath, relativePath); + } + }; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), []); + }); + }); +} diff --git a/src/testRunner/unittests/tsserverHelpers.ts b/src/testRunner/unittests/tsserverHelpers.ts index b6154f46b71..f042a43738a 100644 --- a/src/testRunner/unittests/tsserverHelpers.ts +++ b/src/testRunner/unittests/tsserverHelpers.ts @@ -450,19 +450,15 @@ namespace ts.projectSystem { } export const nodeModules = "node_modules"; - //function getNodeModuleDirectories(dir: string) { - // return getRootsToWatchWithAncestorDirectory(dir, nodeModules); - //} + export 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); - //} - //function checkOpenFiles(projectService: server.ProjectService, expectedFiles: File[]) { // checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path)); //} diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 624b11d8d99..f30e8ad5d9c 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -1,12 +1,4 @@ namespace ts.projectSystem { - function getNodeModuleDirectories(dir: string) { - return getRootsToWatchWithAncestorDirectory(dir, nodeModules); - } - - function getNumberOfWatchesInvokedForRecursiveWatches(recursiveWatchedDirs: string[], file: string) { - return countWhere(recursiveWatchedDirs, dir => file.length > dir.length && startsWith(file, dir) && file[dir.length] === directorySeparator); - } - function checkOpenFiles(projectService: server.ProjectService, expectedFiles: File[]) { checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path)); } @@ -5198,705 +5190,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem CachingFileSystemInformation", () => { - enum CalledMapsWithSingleArg { - fileExists = "fileExists", - directoryExists = "directoryExists", - getDirectories = "getDirectories", - readFile = "readFile" - } - enum CalledMapsWithFiveArgs { - readDirectory = "readDirectory" - } - type CalledMaps = CalledMapsWithSingleArg | CalledMapsWithFiveArgs; - function createCallsTrackingHost(host: TestServerHost) { - const calledMaps: Record> & Record, ReadonlyArray, ReadonlyArray, number]>> = { - fileExists: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.fileExists), - directoryExists: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.directoryExists), - getDirectories: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.getDirectories), - readFile: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.readFile), - readDirectory: setCallsTrackingWithFiveArgFn(CalledMapsWithFiveArgs.readDirectory) - }; - - return { - verifyNoCall, - verifyCalledOnEachEntryNTimes, - verifyCalledOnEachEntry, - verifyNoHostCalls, - verifyNoHostCallsExceptFileExistsOnce, - verifyCalledOn, - clear - }; - - function setCallsTrackingWithSingleArgFn(prop: CalledMapsWithSingleArg) { - const calledMap = createMultiMap(); - const cb = (host)[prop].bind(host); - (host)[prop] = (f: string) => { - calledMap.add(f, /*value*/ true); - return cb(f); - }; - return calledMap; - } - - function setCallsTrackingWithFiveArgFn(prop: CalledMapsWithFiveArgs) { - const calledMap = createMultiMap<[U, V, W, X]>(); - const cb = (host)[prop].bind(host); - (host)[prop] = (f: string, arg1?: U, arg2?: V, arg3?: W, arg4?: X) => { - calledMap.add(f, [arg1!, arg2!, arg3!, arg4!]); // TODO: GH#18217 - return cb(f, arg1, arg2, arg3, arg4); - }; - return calledMap; - } - - function verifyCalledOn(callback: CalledMaps, name: string) { - const calledMap = calledMaps[callback]; - const result = calledMap.get(name); - assert.isTrue(result && !!result.length, `${callback} should be called with name: ${name}: ${arrayFrom(calledMap.keys())}`); - } - - function verifyNoCall(callback: CalledMaps) { - const calledMap = calledMaps[callback]; - assert.equal(calledMap.size, 0, `${callback} shouldn't be called: ${arrayFrom(calledMap.keys())}`); - } - - function verifyCalledOnEachEntry(callback: CalledMaps, expectedKeys: Map) { - TestFSWithWatch.checkMultiMapKeyCount(callback, calledMaps[callback], expectedKeys); - } - - function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: ReadonlyArray, nTimes: number) { - TestFSWithWatch.checkMultiMapKeyCount(callback, calledMaps[callback], expectedKeys, nTimes); - } - - function verifyNoHostCalls() { - iterateOnCalledMaps(key => verifyNoCall(key)); - } - - function verifyNoHostCallsExceptFileExistsOnce(expectedKeys: ReadonlyArray) { - verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, expectedKeys, 1); - verifyNoCall(CalledMapsWithSingleArg.directoryExists); - verifyNoCall(CalledMapsWithSingleArg.getDirectories); - verifyNoCall(CalledMapsWithSingleArg.readFile); - verifyNoCall(CalledMapsWithFiveArgs.readDirectory); - } - - function clear() { - iterateOnCalledMaps(key => calledMaps[key].clear()); - } - - function iterateOnCalledMaps(cb: (key: CalledMaps) => void) { - for (const key in CalledMapsWithSingleArg) { - cb(key as CalledMapsWithSingleArg); - } - for (const key in CalledMapsWithFiveArgs) { - cb(key as CalledMapsWithFiveArgs); - } - } - } - - it("works using legacy resolution logic", () => { - let rootContent = `import {x} from "f1"`; - const root: File = { - path: "/c/d/f0.ts", - content: rootContent - }; - - const imported: File = { - path: "/c/f1.ts", - content: `foo()` - }; - - const host = createServerHost([root, imported]); - const projectService = createProjectService(host); - projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true }); - projectService.openClientFile(root.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const project = projectService.inferredProjects[0]; - const rootScriptInfo = project.getRootScriptInfos()[0]; - assert.equal(rootScriptInfo.fileName, root.path); - - // ensure that imported file was found - verifyImportedDiagnostics(); - - const callsTrackingHost = createCallsTrackingHost(host); - - // trigger synchronization to make sure that import will be fetched from the cache - // ensure file has correct number of errors after edit - editContent(`import {x} from "f1"; - var x: string = 1;`); - verifyImportedDiagnostics(); - callsTrackingHost.verifyNoHostCalls(); - - // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk - editContent(`import {x} from "f2"`); - try { - // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk - verifyImportedDiagnostics(); - assert.isTrue(false, `should not find file '${imported.path}'`); - } - catch (e) { - assert.isTrue(e.message.indexOf(`Could not find file: '${imported.path}'.`) === 0); - } - const f2Lookups = getLocationsForModuleLookup("f2"); - callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, f2Lookups, 1); - const f2DirLookups = getLocationsForDirectoryLookup(); - callsTrackingHost.verifyCalledOnEachEntry(CalledMapsWithSingleArg.directoryExists, f2DirLookups); - callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); - callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.readFile); - callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); - - editContent(`import {x} from "f1"`); - verifyImportedDiagnostics(); - const f1Lookups = f2Lookups.map(s => s.replace("f2", "f1")); - f1Lookups.length = f1Lookups.indexOf(imported.path) + 1; - const f1DirLookups = ["/c/d", "/c", ...mapCombinedPathsInAncestor(getDirectoryPath(root.path), nodeModulesAtTypes, returnTrue)]; - vertifyF1Lookups(); - - // setting compiler options discards module resolution cache - callsTrackingHost.clear(); - projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true, target: ScriptTarget.ES5 }); - verifyImportedDiagnostics(); - vertifyF1Lookups(); - - function vertifyF1Lookups() { - callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, f1Lookups, 1); - callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.directoryExists, f1DirLookups, 1); - callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); - callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.readFile); - callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); - } - - function editContent(newContent: string) { - callsTrackingHost.clear(); - rootScriptInfo.editContent(0, rootContent.length, newContent); - rootContent = newContent; - } - - function verifyImportedDiagnostics() { - const diags = project.getLanguageService().getSemanticDiagnostics(imported.path); - assert.equal(diags.length, 1); - const diag = diags[0]; - assert.equal(diag.code, Diagnostics.Cannot_find_name_0.code); - assert.equal(flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find name 'foo'."); - } - - function getLocationsForModuleLookup(module: string) { - const locations: string[] = []; - forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { - locations.push( - combinePaths(ancestor, `${module}.ts`), - combinePaths(ancestor, `${module}.tsx`), - combinePaths(ancestor, `${module}.d.ts`) - ); - }); - forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { - locations.push( - combinePaths(ancestor, `${module}.js`), - combinePaths(ancestor, `${module}.jsx`) - ); - }); - return locations; - } - - function getLocationsForDirectoryLookup() { - const result = createMap(); - forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { - // To resolve modules - result.set(ancestor, 2); - // for type roots - result.set(combinePaths(ancestor, nodeModules), 1); - result.set(combinePaths(ancestor, nodeModulesAtTypes), 1); - }); - return result; - } - }); - - it("loads missing files from disk", () => { - const root: File = { - path: "/c/foo.ts", - content: `import {y} from "bar"` - }; - - const imported: File = { - path: "/c/bar.d.ts", - content: `export var y = 1` - }; - - const host = createServerHost([root]); - const projectService = createProjectService(host); - projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true }); - const callsTrackingHost = createCallsTrackingHost(host); - projectService.openClientFile(root.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const project = projectService.inferredProjects[0]; - const rootScriptInfo = project.getRootScriptInfos()[0]; - assert.equal(rootScriptInfo.fileName, root.path); - - let diags = project.getLanguageService().getSemanticDiagnostics(root.path); - assert.equal(diags.length, 1); - const diag = diags[0]; - assert.equal(diag.code, Diagnostics.Cannot_find_module_0.code); - assert.equal(flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find module 'bar'."); - callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); - - - callsTrackingHost.clear(); - host.reloadFS([root, imported]); - host.runQueuedTimeoutCallbacks(); - diags = project.getLanguageService().getSemanticDiagnostics(root.path); - assert.equal(diags.length, 0); - callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); - }); - - it("when calling goto definition of module", () => { - const clientFile: File = { - path: "/a/b/controllers/vessels/client.ts", - content: ` - import { Vessel } from '~/models/vessel'; - const v = new Vessel(); - ` - }; - const anotherModuleFile: File = { - path: "/a/b/utils/db.ts", - content: "export class Bookshelf { }" - }; - const moduleFile: File = { - path: "/a/b/models/vessel.ts", - content: ` - import { Bookshelf } from '~/utils/db'; - export class Vessel extends Bookshelf {} - ` - }; - const tsconfigFile: File = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - target: "es6", - module: "es6", - baseUrl: "./", // all paths are relative to the baseUrl - paths: { - "~/*": ["*"] // resolve any `~/foo/bar` to `/foo/bar` - } - }, - exclude: [ - "api", - "build", - "node_modules", - "public", - "seeds", - "sql_updates", - "tests.build" - ] - }) - }; - const projectFiles = [clientFile, anotherModuleFile, moduleFile, tsconfigFile]; - const host = createServerHost(projectFiles); - const session = createSession(host); - const projectService = session.getProjectService(); - const { configFileName } = projectService.openClientFile(clientFile.path); - - assert.isDefined(configFileName, `should find config`); - checkNumberOfConfiguredProjects(projectService, 1); - - const project = projectService.configuredProjects.get(tsconfigFile.path)!; - checkProjectActualFiles(project, map(projectFiles, f => f.path)); - - const callsTrackingHost = createCallsTrackingHost(host); - - // Get definitions shouldnt make host requests - const getDefinitionRequest = makeSessionRequest(protocol.CommandTypes.Definition, { - file: clientFile.path, - position: clientFile.content.indexOf("/vessel") + 1, - line: undefined!, // TODO: GH#18217 - offset: undefined! // TODO: GH#18217 - }); - 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(); - - // Open the file should call only file exists on module directory and use cached value for parental directory - const { configFileName: config2 } = projectService.openClientFile(moduleFile.path); - assert.equal(config2, configFileName); - callsTrackingHost.verifyNoHostCallsExceptFileExistsOnce(["/a/b/models/tsconfig.json", "/a/b/models/jsconfig.json"]); - - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(tsconfigFile.path), project); - }); - - describe("WatchDirectories for config file with", () => { - function verifyWatchDirectoriesCaseSensitivity(useCaseSensitiveFileNames: boolean) { - const frontendDir = "/Users/someuser/work/applications/frontend"; - const toCanonical: (s: string) => Path = useCaseSensitiveFileNames ? s => s as Path : s => s.toLowerCase() as Path; - const canonicalFrontendDir = toCanonical(frontendDir); - const file1: File = { - path: `${frontendDir}/src/app/utils/Analytic.ts`, - content: "export class SomeClass { };" - }; - const file2: File = { - path: `${frontendDir}/src/app/redux/configureStore.ts`, - content: "export class configureStore { }" - }; - const file3: File = { - path: `${frontendDir}/src/app/utils/Cookie.ts`, - content: "export class Cookie { }" - }; - const es2016LibFile: File = { - path: "/a/lib/lib.es2016.full.d.ts", - content: libFile.content - }; - const typeRoots = ["types", "node_modules/@types"]; - const types = ["node", "jest"]; - const tsconfigFile: File = { - path: `${frontendDir}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - strict: true, - strictNullChecks: true, - target: "es2016", - module: "commonjs", - moduleResolution: "node", - sourceMap: true, - noEmitOnError: true, - experimentalDecorators: true, - emitDecoratorMetadata: true, - types, - noUnusedLocals: true, - outDir: "./compiled", - typeRoots, - baseUrl: ".", - paths: { - "*": [ - "types/*" - ] - } - }, - include: [ - "src/**/*" - ], - exclude: [ - "node_modules", - "compiled" - ] - }) - }; - const projectFiles = [file1, file2, es2016LibFile, tsconfigFile]; - const host = createServerHost(projectFiles, { useCaseSensitiveFileNames }); - const projectService = createProjectService(host); - const canonicalConfigPath = toCanonical(tsconfigFile.path); - const { configFileName } = projectService.openClientFile(file1.path); - assert.equal(configFileName, tsconfigFile.path as server.NormalizedPath, `should find config`); // tslint:disable-line no-unnecessary-type-assertion (TODO: GH#18217) - checkNumberOfConfiguredProjects(projectService, 1); - const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, `${canonicalFrontendDir}/types`, `${canonicalFrontendDir}/node_modules`].concat(getNodeModuleDirectories(getDirectoryPath(canonicalFrontendDir))); - - const project = projectService.configuredProjects.get(canonicalConfigPath)!; - verifyProjectAndWatchedDirectories(); - - const callsTrackingHost = createCallsTrackingHost(host); - - // Create file cookie.ts - projectFiles.push(file3); - host.reloadFS(projectFiles); - host.runQueuedTimeoutCallbacks(); - - const canonicalFile3Path = useCaseSensitiveFileNames ? file3.path : file3.path.toLocaleLowerCase(); - const numberOfTimesWatchInvoked = getNumberOfWatchesInvokedForRecursiveWatches(watchingRecursiveDirectories, canonicalFile3Path); - callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, [canonicalFile3Path], numberOfTimesWatchInvoked); - callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.directoryExists, [canonicalFile3Path], numberOfTimesWatchInvoked); - callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); - callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.readFile, [file3.path], 1); - callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); - - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); - verifyProjectAndWatchedDirectories(); - - callsTrackingHost.clear(); - - const { configFileName: configFile2 } = projectService.openClientFile(file3.path); - assert.equal(configFile2, configFileName); - - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); - verifyProjectAndWatchedDirectories(); - callsTrackingHost.verifyNoHostCalls(); - - function getFilePathIfNotOpen(f: File) { - const path = toCanonical(f.path); - const info = projectService.getScriptInfoForPath(toCanonical(f.path)); - return info && info.isScriptOpen() ? undefined : path; - } - - function verifyProjectAndWatchedDirectories() { - checkProjectActualFiles(project, map(projectFiles, f => f.path)); - checkWatchedFiles(host, mapDefined(projectFiles, getFilePathIfNotOpen)); - checkWatchedDirectories(host, watchingRecursiveDirectories, /*recursive*/ true); - checkWatchedDirectories(host, [], /*recursive*/ false); - } - } - - it("case insensitive file system", () => { - verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ false); - }); - - it("case sensitive file system", () => { - verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ true); - }); - }); - - describe("Subfolder invalidations correctly include parent folder failed lookup locations", () => { - function runFailedLookupTest(resolution: "Node" | "Classic") { - const projectLocation = "/proj"; - const file1: File = { - path: `${projectLocation}/foo/boo/app.ts`, - content: `import * as debug from "debug"` - }; - const file2: File = { - path: `${projectLocation}/foo/boo/moo/app.ts`, - content: `import * as debug from "debug"` - }; - const tsconfig: File = { - path: `${projectLocation}/tsconfig.json`, - content: JSON.stringify({ - files: ["foo/boo/app.ts", "foo/boo/moo/app.ts"], - moduleResolution: resolution - }) - }; - - const files = [file1, file2, tsconfig, libFile]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(file1.path); - - const project = service.configuredProjects.get(tsconfig.path)!; - checkProjectActualFiles(project, files.map(f => f.path)); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); - - const debugTypesFile: File = { - path: `${projectLocation}/node_modules/debug/index.d.ts`, - content: "export {}" - }; - files.push(debugTypesFile); - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkProjectActualFiles(project, files.map(f => f.path)); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), []); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), []); - } - - it("Includes the parent folder FLLs in node module resolution mode", () => { - runFailedLookupTest("Node"); - }); - it("Includes the parent folder FLLs in classic module resolution mode", () => { - runFailedLookupTest("Classic"); - }); - }); - - describe("Verify npm install in directory with tsconfig file works when", () => { - function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { - const root = "/user/username/rootfolder/otherfolder"; - const getRootedFileOrFolder = (fileOrFolder: File) => { - fileOrFolder.path = root + fileOrFolder.path; - return fileOrFolder; - }; - const app: File = getRootedFileOrFolder({ - path: "/a/b/app.ts", - content: "import _ from 'lodash';" - }); - const tsconfigJson: File = getRootedFileOrFolder({ - path: "/a/b/tsconfig.json", - content: '{ "compilerOptions": { } }' - }); - const packageJson: File = getRootedFileOrFolder({ - path: "/a/b/package.json", - content: ` -{ - "name": "test", - "version": "1.0.0", - "description": "", - "main": "index.js", - "dependencies": { - "lodash", - "rxjs" - }, - "devDependencies": { - "@types/lodash", - "typescript" - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC" -} -` - }); - const appFolder = getDirectoryPath(app.path); - const projectFiles = [app, libFile, tsconfigJson]; - const typeRootDirectories = getTypeRootsFromLocation(getDirectoryPath(tsconfigJson.path)); - const otherFiles = [packageJson]; - const host = createServerHost(projectFiles.concat(otherFiles)); - const projectService = createProjectService(host); - const { configFileName } = projectService.openClientFile(app.path); - assert.equal(configFileName, tsconfigJson.path as server.NormalizedPath, `should find config`); // TODO: GH#18217 - const recursiveWatchedDirectories: string[] = [`${appFolder}`, `${appFolder}/node_modules`].concat(getNodeModuleDirectories(getDirectoryPath(appFolder))); - verifyProject(); - - let npmInstallComplete = false; - - // Simulate npm install - const filesAndFoldersToAdd: File[] = [ - { path: "/a/b/node_modules" }, - { path: "/a/b/node_modules/.staging/@types" }, - { path: "/a/b/node_modules/.staging/lodash-b0733faa" }, - { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7" }, - { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff" }, - { path: "/a/b/node_modules/.staging/rxjs-22375c61" }, - { path: "/a/b/node_modules/.staging/typescript-8493ea5d" }, - { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/package.json", content: "{\n \"name\": \"symbol-observable\",\n \"version\": \"1.0.4\",\n \"description\": \"Symbol.observable ponyfill\",\n \"license\": \"MIT\",\n \"repository\": \"blesh/symbol-observable\",\n \"author\": {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"engines\": {\n \"node\": \">=0.10.0\"\n },\n \"scripts\": {\n \"test\": \"npm run build && mocha && tsc ./ts-test/test.ts && node ./ts-test/test.js && check-es3-syntax -p lib/ --kill\",\n \"build\": \"babel es --out-dir lib\",\n \"prepublish\": \"npm test\"\n },\n \"files\": [\n \"" }, - { path: "/a/b/node_modules/.staging/lodash-b0733faa/package.json", content: "{\n \"name\": \"lodash\",\n \"version\": \"4.17.4\",\n \"description\": \"Lodash modular utilities.\",\n \"keywords\": \"modules, stdlib, util\",\n \"homepage\": \"https://lodash.com/\",\n \"repository\": \"lodash/lodash\",\n \"icon\": \"https://lodash.com/icon.svg\",\n \"license\": \"MIT\",\n \"main\": \"lodash.js\",\n \"author\": \"John-David Dalton (http://allyoucanleet.com/)\",\n \"contributors\": [\n \"John-David Dalton (http://allyoucanleet.com/)\",\n \"Mathias Bynens \",\n \"contributors\": [\n {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n {\n \"name\": \"Paul Taylor\",\n \"email\": \"paul.e.taylor@me.com\"\n },\n {\n \"name\": \"Jeff Cross\",\n \"email\": \"crossj@google.com\"\n },\n {\n \"name\": \"Matthew Podwysocki\",\n \"email\": \"matthewp@microsoft.com\"\n },\n {\n \"name\": \"OJ Kwon\",\n \"email\": \"kwon.ohjoong@gmail.com\"\n },\n {\n \"name\": \"Andre Staltz\",\n \"email\": \"andre@staltz.com\"\n }\n ],\n \"license\": \"Apache-2.0\",\n \"bugs\": {\n \"url\": \"https://github.com/ReactiveX/RxJS/issues\"\n },\n \"homepage\": \"https://github.com/ReactiveX/RxJS\",\n \"devDependencies\": {\n \"babel-polyfill\": \"^6.23.0\",\n \"benchmark\": \"^2.1.0\",\n \"benchpress\": \"2.0.0-beta.1\",\n \"chai\": \"^3.5.0\",\n \"color\": \"^0.11.1\",\n \"colors\": \"1.1.2\",\n \"commitizen\": \"^2.8.6\",\n \"coveralls\": \"^2.11.13\",\n \"cz-conventional-changelog\": \"^1.2.0\",\n \"danger\": \"^1.1.0\",\n \"doctoc\": \"^1.0.0\",\n \"escape-string-regexp\": \"^1.0.5 \",\n \"esdoc\": \"^0.4.7\",\n \"eslint\": \"^3.8.0\",\n \"fs-extra\": \"^2.1.2\",\n \"get-folder-size\": \"^1.0.0\",\n \"glob\": \"^7.0.3\",\n \"gm\": \"^1.22.0\",\n \"google-closure-compiler-js\": \"^20170218.0.0\",\n \"gzip-size\": \"^3.0.0\",\n \"http-server\": \"^0.9.0\",\n \"husky\": \"^0.13.3\",\n \"lint-staged\": \"3.2.5\",\n \"lodash\": \"^4.15.0\",\n \"madge\": \"^1.4.3\",\n \"markdown-doctest\": \"^0.9.1\",\n \"minimist\": \"^1.2.0\",\n \"mkdirp\": \"^0.5.1\",\n \"mocha\": \"^3.0.2\",\n \"mocha-in-sauce\": \"0.0.1\",\n \"npm-run-all\": \"^4.0.2\",\n \"npm-scripts-info\": \"^0.3.4\",\n \"nyc\": \"^10.2.0\",\n \"opn-cli\": \"^3.1.0\",\n \"platform\": \"^1.3.1\",\n \"promise\": \"^7.1.1\",\n \"protractor\": \"^3.1.1\",\n \"rollup\": \"0.36.3\",\n \"rollup-plugin-inject\": \"^2.0.0\",\n \"rollup-plugin-node-resolve\": \"^2.0.0\",\n \"rx\": \"latest\",\n \"rxjs\": \"latest\",\n \"shx\": \"^0.2.2\",\n \"sinon\": \"^2.1.0\",\n \"sinon-chai\": \"^2.9.0\",\n \"source-map-support\": \"^0.4.0\",\n \"tslib\": \"^1.5.0\",\n \"tslint\": \"^4.4.2\",\n \"typescript\": \"~2.0.6\",\n \"typings\": \"^2.0.0\",\n \"validate-commit-msg\": \"^2.14.0\",\n \"watch\": \"^1.0.1\",\n \"webpack\": \"^1.13.1\",\n \"xmlhttprequest\": \"1.8.0\"\n },\n \"engines\": {\n \"npm\": \">=2.0.0\"\n },\n \"typings\": \"Rx.d.ts\",\n \"dependencies\": {\n \"symbol-observable\": \"^1.0.1\"\n }\n}" }, - { path: "/a/b/node_modules/.staging/typescript-8493ea5d/package.json", content: "{\n \"name\": \"typescript\",\n \"author\": \"Microsoft Corp.\",\n \"homepage\": \"http://typescriptlang.org/\",\n \"version\": \"2.4.2\",\n \"license\": \"Apache-2.0\",\n \"description\": \"TypeScript is a language for application scale JavaScript development\",\n \"keywords\": [\n \"TypeScript\",\n \"Microsoft\",\n \"compiler\",\n \"language\",\n \"javascript\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/Microsoft/TypeScript/issues\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/Microsoft/TypeScript.git\"\n },\n \"main\": \"./lib/typescript.js\",\n \"typings\": \"./lib/typescript.d.ts\",\n \"bin\": {\n \"tsc\": \"./bin/tsc\",\n \"tsserver\": \"./bin/tsserver\"\n },\n \"engines\": {\n \"node\": \">=4.2.0\"\n },\n \"devDependencies\": {\n \"@types/browserify\": \"latest\",\n \"@types/chai\": \"latest\",\n \"@types/convert-source-map\": \"latest\",\n \"@types/del\": \"latest\",\n \"@types/glob\": \"latest\",\n \"@types/gulp\": \"latest\",\n \"@types/gulp-concat\": \"latest\",\n \"@types/gulp-help\": \"latest\",\n \"@types/gulp-newer\": \"latest\",\n \"@types/gulp-sourcemaps\": \"latest\",\n \"@types/merge2\": \"latest\",\n \"@types/minimatch\": \"latest\",\n \"@types/minimist\": \"latest\",\n \"@types/mkdirp\": \"latest\",\n \"@types/mocha\": \"latest\",\n \"@types/node\": \"latest\",\n \"@types/q\": \"latest\",\n \"@types/run-sequence\": \"latest\",\n \"@types/through2\": \"latest\",\n \"browserify\": \"latest\",\n \"chai\": \"latest\",\n \"convert-source-map\": \"latest\",\n \"del\": \"latest\",\n \"gulp\": \"latest\",\n \"gulp-clone\": \"latest\",\n \"gulp-concat\": \"latest\",\n \"gulp-help\": \"latest\",\n \"gulp-insert\": \"latest\",\n \"gulp-newer\": \"latest\",\n \"gulp-sourcemaps\": \"latest\",\n \"gulp-typescript\": \"latest\",\n \"into-stream\": \"latest\",\n \"istanbul\": \"latest\",\n \"jake\": \"latest\",\n \"merge2\": \"latest\",\n \"minimist\": \"latest\",\n \"mkdirp\": \"latest\",\n \"mocha\": \"latest\",\n \"mocha-fivemat-progress-reporter\": \"latest\",\n \"q\": \"latest\",\n \"run-sequence\": \"latest\",\n \"sorcery\": \"latest\",\n \"through2\": \"latest\",\n \"travis-fold\": \"latest\",\n \"ts-node\": \"latest\",\n \"tslint\": \"latest\",\n \"typescript\": \"^2.4\"\n },\n \"scripts\": {\n \"pretest\": \"jake tests\",\n \"test\": \"jake runtests-parallel\",\n \"build\": \"npm run build:compiler && npm run build:tests\",\n \"build:compiler\": \"jake local\",\n \"build:tests\": \"jake tests\",\n \"start\": \"node lib/tsc\",\n \"clean\": \"jake clean\",\n \"gulp\": \"gulp\",\n \"jake\": \"jake\",\n \"lint\": \"jake lint\",\n \"setup-hooks\": \"node scripts/link-hooks.js\"\n },\n \"browser\": {\n \"buffer\": false,\n \"fs\": false,\n \"os\": false,\n \"path\": false\n }\n}" }, - { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/index.js", content: "module.exports = require('./lib/index');\n" }, - { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/index.d.ts", content: "declare const observableSymbol: symbol;\nexport default observableSymbol;\n" }, - { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/lib" }, - { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/lib/index.js", content: "'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _ponyfill = require('./ponyfill');\n\nvar _ponyfill2 = _interopRequireDefault(_ponyfill);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\nvar root; /* global window */\n\n\nif (typeof self !== 'undefined') {\n root = self;\n} else if (typeof window !== 'undefined') {\n root = window;\n} else if (typeof global !== 'undefined') {\n root = global;\n} else if (typeof module !== 'undefined') {\n root = module;\n} else {\n root = Function('return this')();\n}\n\nvar result = (0, _ponyfill2['default'])(root);\nexports['default'] = result;" }, - ].map(getRootedFileOrFolder); - verifyAfterPartialOrCompleteNpmInstall(2); - - filesAndFoldersToAdd.push(...[ - { path: "/a/b/node_modules/.staging/typescript-8493ea5d/lib" }, - { path: "/a/b/node_modules/.staging/rxjs-22375c61/add/operator" }, - { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/package.json", content: "{\n \"name\": \"@types/lodash\",\n \"version\": \"4.14.74\",\n \"description\": \"TypeScript definitions for Lo-Dash\",\n \"license\": \"MIT\",\n \"contributors\": [\n {\n \"name\": \"Brian Zengel\",\n \"url\": \"https://github.com/bczengel\"\n },\n {\n \"name\": \"Ilya Mochalov\",\n \"url\": \"https://github.com/chrootsu\"\n },\n {\n \"name\": \"Stepan Mikhaylyuk\",\n \"url\": \"https://github.com/stepancar\"\n },\n {\n \"name\": \"Eric L Anderson\",\n \"url\": \"https://github.com/ericanderson\"\n },\n {\n \"name\": \"AJ Richardson\",\n \"url\": \"https://github.com/aj-r\"\n },\n {\n \"name\": \"Junyoung Clare Jang\",\n \"url\": \"https://github.com/ailrun\"\n }\n ],\n \"main\": \"\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://www.github.com/DefinitelyTyped/DefinitelyTyped.git\"\n },\n \"scripts\": {},\n \"dependencies\": {},\n \"typesPublisherContentHash\": \"12af578ffaf8d86d2df37e591857906a86b983fa9258414326544a0fe6af0de8\",\n \"typeScriptVersion\": \"2.2\"\n}" }, - { path: "/a/b/node_modules/.staging/lodash-b0733faa/index.js", content: "module.exports = require('./lodash');" }, - { path: "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" } - ].map(getRootedFileOrFolder)); - // Since we added/removed in .staging no timeout - verifyAfterPartialOrCompleteNpmInstall(0); - - // Remove file "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" - filesAndFoldersToAdd.length--; - verifyAfterPartialOrCompleteNpmInstall(0); - - filesAndFoldersToAdd.push(...[ - { path: "/a/b/node_modules/.staging/rxjs-22375c61/bundles" }, - { path: "/a/b/node_modules/.staging/rxjs-22375c61/operator" }, - { path: "/a/b/node_modules/.staging/rxjs-22375c61/src/add/observable/dom" }, - { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/index.d.ts", content: "\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(0); - - 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(...[ - { path: "/a/b/node_modules/symbol-observable" }, - { path: "/a/b/node_modules/@types" }, - { path: "/a/b/node_modules/@types/lodash" }, - { path: "/a/b/node_modules/lodash" }, - { path: "/a/b/node_modules/rxjs" }, - { path: "/a/b/node_modules/typescript" }, - { path: "/a/b/node_modules/.bin" } - ].map(getRootedFileOrFolder)); - // From the type root update - verifyAfterPartialOrCompleteNpmInstall(2); - - forEach(filesAndFoldersToAdd, f => { - f.path = f.path - .replace("/a/b/node_modules/.staging", "/a/b/node_modules") - .replace(/[\-\.][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w]/g, ""); - }); - - const lodashIndexPath = 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 = 2; - // npm installation complete, timeout after reload fs - npmInstallComplete = true; - verifyAfterPartialOrCompleteNpmInstall(2); - - function verifyAfterPartialOrCompleteNpmInstall(timeoutQueueLengthWhenRunningTimeouts: number) { - host.reloadFS(projectFiles.concat(otherFiles, filesAndFoldersToAdd)); - if (npmInstallComplete || timeoutDuringPartialInstallation) { - host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts); - } - else { - host.checkTimeoutQueueLength(2); - } - verifyProject(); - } - - function verifyProject() { - checkNumberOfConfiguredProjects(projectService, 1); - - const project = projectService.configuredProjects.get(tsconfigJson.path)!; - const projectFilePaths = map(projectFiles, f => f.path); - checkProjectActualFiles(project, projectFilePaths); - - const filesWatched = filter(projectFilePaths, p => p !== app.path && p.indexOf("/a/b/node_modules") === -1); - checkWatchedFiles(host, filesWatched); - checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true); - checkWatchedDirectories(host, [], /*recursive*/ false); - } - } - - it("timeouts occur inbetween installation", () => { - verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); - }); - - it("timeout occurs after installation", () => { - verifyNpmInstall(/*timeoutDuringPartialInstallation*/ false); - }); - }); - - it("when node_modules dont receive event for the @types file addition", () => { - const projectLocation = "/user/username/folder/myproject"; - const app: File = { - path: `${projectLocation}/app.ts`, - content: `import * as debug from "debug"` - }; - const tsconfig: File = { - path: `${projectLocation}/tsconfig.json`, - content: "" - }; - - const files = [app, tsconfig, libFile]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(app.path); - - const project = service.configuredProjects.get(tsconfig.path)!; - checkProjectActualFiles(project, files.map(f => f.path)); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); - - const debugTypesFile: File = { - path: `${projectLocation}/node_modules/@types/debug/index.d.ts`, - content: "export {}" - }; - files.push(debugTypesFile); - // Do not invoke recursive directory watcher for anything other than node_module/@types - const invoker = host.invokeWatchedDirectoriesRecursiveCallback; - host.invokeWatchedDirectoriesRecursiveCallback = (fullPath, relativePath) => { - if (fullPath.endsWith("@types")) { - invoker.call(host, fullPath, relativePath); - } - }; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkProjectActualFiles(project, files.map(f => f.path)); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), []); - }); - }); - describe("tsserverProjectSystem with symLinks", () => { it("rename in common file renames all project", () => { const projects = "/users/username/projects"; From 5234b8b18e08d6835b90d3b667a77be9c3ccf9b6 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 13:23:26 -0800 Subject: [PATCH 14/20] Move more tests into resolutionCache and project errors --- src/testRunner/unittests/projectErrors.ts | 166 ++-- src/testRunner/unittests/resolutionCache.ts | 848 +++++++++--------- .../unittests/tsserverProjectSystem.ts | 46 - 3 files changed, 530 insertions(+), 530 deletions(-) diff --git a/src/testRunner/unittests/projectErrors.ts b/src/testRunner/unittests/projectErrors.ts index 9ff7278e78c..cb82a9eeca6 100644 --- a/src/testRunner/unittests/projectErrors.ts +++ b/src/testRunner/unittests/projectErrors.ts @@ -481,89 +481,6 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: Project Errors dont include overwrite emit error", () => { - it("for inferred project", () => { - const f1 = { - path: "/a/b/f1.js", - content: "function test1() { }" - }; - const host = createServerHost([f1, libFile]); - const session = createSession(host); - openFilesForSession([f1], session); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const projectName = projectService.inferredProjects[0].getProjectName(); - - const diags = session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName: projectName } - }).response as ReadonlyArray; - assert.isTrue(diags.length === 0); - - session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsForInferredProjects, - seq: 3, - arguments: { options: { module: ModuleKind.CommonJS } } - }); - const diagsAfterUpdate = session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 4, - arguments: { projectFileName: projectName } - }).response as ReadonlyArray; - assert.isTrue(diagsAfterUpdate.length === 0); - }); - - it("for external project", () => { - const f1 = { - path: "/a/b/f1.js", - content: "function test1() { }" - }; - const host = createServerHost([f1, libFile]); - const session = createSession(host); - const projectService = session.getProjectService(); - const projectFileName = "/a/b/project.csproj"; - const externalFiles = toExternalFiles([f1.path]); - projectService.openExternalProject({ - projectFileName, - rootFiles: externalFiles, - options: {} - }); - - checkNumberOfProjects(projectService, { externalProjects: 1 }); - - const diags = session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName } - }).response as ReadonlyArray; - assert.isTrue(diags.length === 0); - - session.executeCommand({ - type: "request", - command: server.CommandNames.OpenExternalProject, - seq: 3, - arguments: { - projectFileName, - rootFiles: externalFiles, - options: { module: ModuleKind.CommonJS } - } - }); - const diagsAfterUpdate = session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 4, - arguments: { projectFileName } - }).response as ReadonlyArray; - assert.isTrue(diagsAfterUpdate.length === 0); - }); - }); - describe("tsserver:: Project Errors for Configure file diagnostics events", () => { function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string): ConfigFileDiagnostic { const d = Diagnostics.Unknown_compiler_option_0; @@ -754,6 +671,89 @@ namespace ts.projectSystem { }); }); + describe("tsserver:: Project Errors dont include overwrite emit error", () => { + it("for inferred project", () => { + const f1 = { + path: "/a/b/f1.js", + content: "function test1() { }" + }; + const host = createServerHost([f1, libFile]); + const session = createSession(host); + openFilesForSession([f1], session); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const projectName = projectService.inferredProjects[0].getProjectName(); + + const diags = session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName: projectName } + }).response as ReadonlyArray; + assert.isTrue(diags.length === 0); + + session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsForInferredProjects, + seq: 3, + arguments: { options: { module: ModuleKind.CommonJS } } + }); + const diagsAfterUpdate = session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 4, + arguments: { projectFileName: projectName } + }).response as ReadonlyArray; + assert.isTrue(diagsAfterUpdate.length === 0); + }); + + it("for external project", () => { + const f1 = { + path: "/a/b/f1.js", + content: "function test1() { }" + }; + const host = createServerHost([f1, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); + const projectFileName = "/a/b/project.csproj"; + const externalFiles = toExternalFiles([f1.path]); + projectService.openExternalProject({ + projectFileName, + rootFiles: externalFiles, + options: {} + }); + + checkNumberOfProjects(projectService, { externalProjects: 1 }); + + const diags = session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName } + }).response as ReadonlyArray; + assert.isTrue(diags.length === 0); + + session.executeCommand({ + type: "request", + command: server.CommandNames.OpenExternalProject, + seq: 3, + arguments: { + projectFileName, + rootFiles: externalFiles, + options: { module: ModuleKind.CommonJS } + } + }); + const diagsAfterUpdate = session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 4, + arguments: { projectFileName } + }).response as ReadonlyArray; + assert.isTrue(diagsAfterUpdate.length === 0); + }); + }); + describe("tsserver:: Project Errors reports Options Diagnostic locations correctly with changes in configFile contents", () => { it("when options change", () => { const file = { diff --git a/src/testRunner/unittests/resolutionCache.ts b/src/testRunner/unittests/resolutionCache.ts index ab6c6830618..d97770e21b5 100644 --- a/src/testRunner/unittests/resolutionCache.ts +++ b/src/testRunner/unittests/resolutionCache.ts @@ -495,6 +495,453 @@ namespace ts.projectSystem { }); }); + describe("resolutionCache:: tsserverProjectSystem watching @types", () => { + it("works correctly when typings are added or removed", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1;" + }; + const t1 = { + path: "/a/b/node_modules/@types/lib1/index.d.ts", + content: "export let a: number" + }; + const t2 = { + path: "/a/b/node_modules/@types/lib2/index.d.ts", + content: "export let b: number" + }; + const tsconfig = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + exclude: ["node_modules"] + }) + }; + const host = createServerHost([f1, t1, tsconfig]); + const projectService = createProjectService(host); + + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, t1.path, tsconfig.path]); + + // delete t1 + host.reloadFS([f1, tsconfig]); + // run throttled operation + host.runQueuedTimeoutCallbacks(); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]); + + // create t2 + host.reloadFS([f1, tsconfig, t2]); + // run throttled operation + host.runQueuedTimeoutCallbacks(); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, t2.path, tsconfig.path]); + }); + }); + + describe("resolutionCache:: tsserverProjectSystem add the missing module file for inferred project", () => { + it("should remove the `module not found` error", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const host = createServerHost([file1]); + const session = createSession(host); + openFilesForSession([file1], session); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } + ]); + + host.reloadFS([file1, moduleFile]); + host.runQueuedTimeoutCallbacks(); + + // Make a change to trigger the program rebuild + const changeRequest = makeSessionRequest( + server.CommandNames.Change, + { file: file1.path, line: 1, offset: 44, endLine: 1, endOffset: 44, insertString: "\n" } + ); + session.executeCommand(changeRequest); + + // Recheck + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + }); + + it("npm install @types works", () => { + const folderPath = "/a/b/projects/temp"; + const file1: File = { + path: `${folderPath}/a.ts`, + content: 'import f = require("pad"); f;' + }; + const files = [file1, libFile]; + const host = createServerHost(files); + const session = createSession(host, { canUseEvents: true }); + const service = session.getProjectService(); + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { + file: file1.path, + fileContent: file1.content, + scriptKindName: "TS", + projectRootPath: folderPath + } + }); + checkNumberOfProjects(service, { inferredProjects: 1 }); + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [file1.path] + } + }); + + host.checkTimeoutQueueLengthAndRun(1); + checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(); + const startOffset = file1.content.indexOf('"') + 1; + checkErrorMessage(session, "semanticDiag", { + file: file1.path, + diagnostics: [ + createDiagnostic({ line: 1, offset: startOffset }, { line: 1, offset: startOffset + '"pad"'.length }, Diagnostics.Cannot_find_module_0, ["pad"]) + ], + }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + checkErrorMessage(session, "suggestionDiag", { file: file1.path, diagnostics: [] }); + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); + + const padIndex: File = { + path: `${folderPath}/node_modules/@types/pad/index.d.ts`, + content: "export = pad;declare function pad(length: number, text: string, char ?: string): string;" + }; + files.push(padIndex); + host.reloadFS(files, { ignoreWatchInvokedWithTriggerAsFileCreate: true }); + host.runQueuedTimeoutCallbacks(); + checkProjectUpdatedInBackgroundEvent(session, [file1.path]); + session.clearMessages(); + + host.runQueuedTimeoutCallbacks(); + checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(); + checkErrorMessage(session, "semanticDiag", { file: file1.path, diagnostics: [] }); + }); + + it("suggestion diagnostics", () => { + const file: File = { + path: "/a.js", + content: "function f(p) {}", + }; + + const host = createServerHost([file]); + const session = createSession(host, { canUseEvents: true }); + const service = session.getProjectService(); + + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { file: file.path, fileContent: file.content }, + }); + + checkNumberOfProjects(service, { inferredProjects: 1 }); + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + host.checkTimeoutQueueLengthAndRun(2); + + checkProjectUpdatedInBackgroundEvent(session, [file.path]); + session.clearMessages(); + + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [file.path], + } + }); + + host.checkTimeoutQueueLengthAndRun(1); + + checkErrorMessage(session, "syntaxDiag", { file: file.path, diagnostics: [] }, /*isMostRecent*/ true); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + + checkErrorMessage(session, "semanticDiag", { file: file.path, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + + checkErrorMessage(session, "suggestionDiag", { + file: file.path, + diagnostics: [ + createDiagnostic({ line: 1, offset: 12 }, { line: 1, offset: 13 }, Diagnostics._0_is_declared_but_its_value_is_never_read, ["p"], "suggestion", /*reportsUnnecessary*/ true), + ], + }); + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); + }); + + it("disable suggestion diagnostics", () => { + const file: File = { + path: "/a.js", + content: 'require("b")', + }; + + const host = createServerHost([file]); + const session = createSession(host, { canUseEvents: true }); + const service = session.getProjectService(); + + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { file: file.path, fileContent: file.content }, + }); + + session.executeCommandSeq({ + command: server.CommandNames.Configure, + arguments: { + preferences: { disableSuggestions: true } + }, + }); + + checkNumberOfProjects(service, { inferredProjects: 1 }); + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + host.checkTimeoutQueueLengthAndRun(2); + + checkProjectUpdatedInBackgroundEvent(session, [file.path]); + session.clearMessages(); + + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [file.path], + } + }); + + host.checkTimeoutQueueLengthAndRun(1); + + checkErrorMessage(session, "syntaxDiag", { file: file.path, diagnostics: [] }, /*isMostRecent*/ true); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + + checkErrorMessage(session, "semanticDiag", { file: file.path, diagnostics: [] }); + // No suggestion event, we're done. + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); + }); + + it("suppressed diagnostic events", () => { + const file: File = { + path: "/a.ts", + content: "1 = 2;", + }; + + const host = createServerHost([file]); + const session = createSession(host, { canUseEvents: true, suppressDiagnosticEvents: true }); + const service = session.getProjectService(); + + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { file: file.path, fileContent: file.content }, + }); + + checkNumberOfProjects(service, { inferredProjects: 1 }); + + host.checkTimeoutQueueLength(0); + checkNoDiagnosticEvents(session); + + session.clearMessages(); + + let expectedSequenceId = session.getNextSeq(); + + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [file.path], + } + }); + + host.checkTimeoutQueueLength(0); + checkNoDiagnosticEvents(session); + + checkCompleteEvent(session, 1, expectedSequenceId); + + session.clearMessages(); + + expectedSequenceId = session.getNextSeq(); + + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + file: file.path, + } + }); + + host.checkTimeoutQueueLength(0); + checkNoDiagnosticEvents(session); + + checkCompleteEvent(session, 1, expectedSequenceId); + + session.clearMessages(); + }); + }); + + describe("resolutionCache:: tsserverProjectSystem rename a module file and rename back", () => { + it("should restore the states for inferred projects", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const host = createServerHost([moduleFile, file1]); + const session = createSession(host); + + openFilesForSession([file1], session); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + + const moduleFileOldPath = moduleFile.path; + const moduleFileNewPath = "/a/b/moduleFile1.ts"; + moduleFile.path = moduleFileNewPath; + host.reloadFS([moduleFile, file1]); + host.runQueuedTimeoutCallbacks(); + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } + ]); + assert.equal(diags.length, 1); + + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1]); + host.runQueuedTimeoutCallbacks(); + + // Make a change to trigger the program rebuild + const changeRequest = makeSessionRequest( + server.CommandNames.Change, + { file: file1.path, line: 1, offset: 44, endLine: 1, endOffset: 44, insertString: "\n" } + ); + session.executeCommand(changeRequest); + host.runQueuedTimeoutCallbacks(); + + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + }); + + it("should restore the states for configured projects", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([moduleFile, file1, configFile]); + const session = createSession(host); + + openFilesForSession([file1], session); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + + const moduleFileOldPath = moduleFile.path; + const moduleFileNewPath = "/a/b/moduleFile1.ts"; + moduleFile.path = moduleFileNewPath; + host.reloadFS([moduleFile, file1, configFile]); + host.runQueuedTimeoutCallbacks(); + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } + ]); + + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1, configFile]); + host.runQueuedTimeoutCallbacks(); + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + }); + + it("should property handle missing config files", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + const projectName = "project1"; + const host = createServerHost([f1]); + const projectService = createProjectService(host); + projectService.openExternalProject({ rootFiles: toExternalFiles([f1.path, config.path]), options: {}, projectFileName: projectName }); + + // should have one external project since config file is missing + projectService.checkNumberOfProjects({ externalProjects: 1 }); + + host.reloadFS([f1, config]); + projectService.openExternalProject({ rootFiles: toExternalFiles([f1.path, config.path]), options: {}, projectFileName: projectName }); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + }); + + it("types should load from config file path if config exists", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } }) + }; + const node = { + path: "/a/b/node_modules/@types/node/index.d.ts", + content: "declare var process: any" + }; + const cwd = { + path: "/a/c" + }; + const host = createServerHost([f1, config, node, cwd], { currentDirectory: cwd.path }); + const projectService = createProjectService(host); + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, node.path, config.path]); + }); + }); + describe("resolutionCache:: tsserverProjectSystem module resolution caching", () => { const projectLocation = "/user/username/projects/myproject"; const configFile: File = { @@ -978,405 +1425,4 @@ export const x = 10;` }); }); }); - - describe("resolutionCache:: tsserverProjectSystem rename a module file and rename back", () => { - it("should restore the states for inferred projects", () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: "import * as T from './moduleFile'; T.bar();" - }; - const host = createServerHost([moduleFile, file1]); - const session = createSession(host); - - openFilesForSession([file1], session); - const getErrRequest = makeSessionRequest( - server.CommandNames.SemanticDiagnosticsSync, - { file: file1.path } - ); - let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - - const moduleFileOldPath = moduleFile.path; - const moduleFileNewPath = "/a/b/moduleFile1.ts"; - moduleFile.path = moduleFileNewPath; - host.reloadFS([moduleFile, file1]); - host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyDiagnostics(diags, [ - { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } - ]); - assert.equal(diags.length, 1); - - moduleFile.path = moduleFileOldPath; - host.reloadFS([moduleFile, file1]); - host.runQueuedTimeoutCallbacks(); - - // Make a change to trigger the program rebuild - const changeRequest = makeSessionRequest( - server.CommandNames.Change, - { file: file1.path, line: 1, offset: 44, endLine: 1, endOffset: 44, insertString: "\n" } - ); - session.executeCommand(changeRequest); - host.runQueuedTimeoutCallbacks(); - - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - }); - - it("should restore the states for configured projects", () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: "import * as T from './moduleFile'; T.bar();" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createServerHost([moduleFile, file1, configFile]); - const session = createSession(host); - - openFilesForSession([file1], session); - const getErrRequest = makeSessionRequest( - server.CommandNames.SemanticDiagnosticsSync, - { file: file1.path } - ); - let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - - const moduleFileOldPath = moduleFile.path; - const moduleFileNewPath = "/a/b/moduleFile1.ts"; - moduleFile.path = moduleFileNewPath; - host.reloadFS([moduleFile, file1, configFile]); - host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyDiagnostics(diags, [ - { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } - ]); - - moduleFile.path = moduleFileOldPath; - host.reloadFS([moduleFile, file1, configFile]); - host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - }); - - it("should property handle missing config files", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/b/tsconfig.json", - content: "{}" - }; - const projectName = "project1"; - const host = createServerHost([f1]); - const projectService = createProjectService(host); - projectService.openExternalProject({ rootFiles: toExternalFiles([f1.path, config.path]), options: {}, projectFileName: projectName }); - - // should have one external project since config file is missing - projectService.checkNumberOfProjects({ externalProjects: 1 }); - - host.reloadFS([f1, config]); - projectService.openExternalProject({ rootFiles: toExternalFiles([f1.path, config.path]), options: {}, projectFileName: projectName }); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - }); - - it("types should load from config file path if config exists", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } }) - }; - const node = { - path: "/a/b/node_modules/@types/node/index.d.ts", - content: "declare var process: any" - }; - const cwd = { - path: "/a/c" - }; - const host = createServerHost([f1, config, node, cwd], { currentDirectory: cwd.path }); - const projectService = createProjectService(host); - projectService.openClientFile(f1.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, node.path, config.path]); - }); - }); - - describe("resolutionCache:: tsserverProjectSystem add the missing module file for inferred project", () => { - it("should remove the `module not found` error", () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: "import * as T from './moduleFile'; T.bar();" - }; - const host = createServerHost([file1]); - const session = createSession(host); - openFilesForSession([file1], session); - const getErrRequest = makeSessionRequest( - server.CommandNames.SemanticDiagnosticsSync, - { file: file1.path } - ); - let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyDiagnostics(diags, [ - { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } - ]); - - host.reloadFS([file1, moduleFile]); - host.runQueuedTimeoutCallbacks(); - - // Make a change to trigger the program rebuild - const changeRequest = makeSessionRequest( - server.CommandNames.Change, - { file: file1.path, line: 1, offset: 44, endLine: 1, endOffset: 44, insertString: "\n" } - ); - session.executeCommand(changeRequest); - - // Recheck - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - }); - - it("npm install @types works", () => { - const folderPath = "/a/b/projects/temp"; - const file1: File = { - path: `${folderPath}/a.ts`, - content: 'import f = require("pad"); f;' - }; - const files = [file1, libFile]; - const host = createServerHost(files); - const session = createSession(host, { canUseEvents: true }); - const service = session.getProjectService(); - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { - file: file1.path, - fileContent: file1.content, - scriptKindName: "TS", - projectRootPath: folderPath - } - }); - checkNumberOfProjects(service, { inferredProjects: 1 }); - session.clearMessages(); - const expectedSequenceId = session.getNextSeq(); - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [file1.path] - } - }); - - host.checkTimeoutQueueLengthAndRun(1); - checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(); - const startOffset = file1.content.indexOf('"') + 1; - checkErrorMessage(session, "semanticDiag", { - file: file1.path, - diagnostics: [ - createDiagnostic({ line: 1, offset: startOffset }, { line: 1, offset: startOffset + '"pad"'.length }, Diagnostics.Cannot_find_module_0, ["pad"]) - ], - }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - checkErrorMessage(session, "suggestionDiag", { file: file1.path, diagnostics: [] }); - checkCompleteEvent(session, 2, expectedSequenceId); - session.clearMessages(); - - const padIndex: File = { - path: `${folderPath}/node_modules/@types/pad/index.d.ts`, - content: "export = pad;declare function pad(length: number, text: string, char ?: string): string;" - }; - files.push(padIndex); - host.reloadFS(files, { ignoreWatchInvokedWithTriggerAsFileCreate: true }); - host.runQueuedTimeoutCallbacks(); - checkProjectUpdatedInBackgroundEvent(session, [file1.path]); - session.clearMessages(); - - host.runQueuedTimeoutCallbacks(); - checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(); - checkErrorMessage(session, "semanticDiag", { file: file1.path, diagnostics: [] }); - }); - - it("suggestion diagnostics", () => { - const file: File = { - path: "/a.js", - content: "function f(p) {}", - }; - - const host = createServerHost([file]); - const session = createSession(host, { canUseEvents: true }); - const service = session.getProjectService(); - - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { file: file.path, fileContent: file.content }, - }); - - checkNumberOfProjects(service, { inferredProjects: 1 }); - session.clearMessages(); - const expectedSequenceId = session.getNextSeq(); - host.checkTimeoutQueueLengthAndRun(2); - - checkProjectUpdatedInBackgroundEvent(session, [file.path]); - session.clearMessages(); - - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [file.path], - } - }); - - host.checkTimeoutQueueLengthAndRun(1); - - checkErrorMessage(session, "syntaxDiag", { file: file.path, diagnostics: [] }, /*isMostRecent*/ true); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - - checkErrorMessage(session, "semanticDiag", { file: file.path, diagnostics: [] }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - - checkErrorMessage(session, "suggestionDiag", { - file: file.path, - diagnostics: [ - createDiagnostic({ line: 1, offset: 12 }, { line: 1, offset: 13 }, Diagnostics._0_is_declared_but_its_value_is_never_read, ["p"], "suggestion", /*reportsUnnecessary*/ true), - ], - }); - checkCompleteEvent(session, 2, expectedSequenceId); - session.clearMessages(); - }); - - it("disable suggestion diagnostics", () => { - const file: File = { - path: "/a.js", - content: 'require("b")', - }; - - const host = createServerHost([file]); - const session = createSession(host, { canUseEvents: true }); - const service = session.getProjectService(); - - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { file: file.path, fileContent: file.content }, - }); - - session.executeCommandSeq({ - command: server.CommandNames.Configure, - arguments: { - preferences: { disableSuggestions: true } - }, - }); - - checkNumberOfProjects(service, { inferredProjects: 1 }); - session.clearMessages(); - const expectedSequenceId = session.getNextSeq(); - host.checkTimeoutQueueLengthAndRun(2); - - checkProjectUpdatedInBackgroundEvent(session, [file.path]); - session.clearMessages(); - - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [file.path], - } - }); - - host.checkTimeoutQueueLengthAndRun(1); - - checkErrorMessage(session, "syntaxDiag", { file: file.path, diagnostics: [] }, /*isMostRecent*/ true); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - - checkErrorMessage(session, "semanticDiag", { file: file.path, diagnostics: [] }); - // No suggestion event, we're done. - checkCompleteEvent(session, 2, expectedSequenceId); - session.clearMessages(); - }); - - it("suppressed diagnostic events", () => { - const file: File = { - path: "/a.ts", - content: "1 = 2;", - }; - - const host = createServerHost([file]); - const session = createSession(host, { canUseEvents: true, suppressDiagnosticEvents: true }); - const service = session.getProjectService(); - - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { file: file.path, fileContent: file.content }, - }); - - checkNumberOfProjects(service, { inferredProjects: 1 }); - - host.checkTimeoutQueueLength(0); - checkNoDiagnosticEvents(session); - - session.clearMessages(); - - let expectedSequenceId = session.getNextSeq(); - - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [file.path], - } - }); - - host.checkTimeoutQueueLength(0); - checkNoDiagnosticEvents(session); - - checkCompleteEvent(session, 1, expectedSequenceId); - - session.clearMessages(); - - expectedSequenceId = session.getNextSeq(); - - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - file: file.path, - } - }); - - host.checkTimeoutQueueLength(0); - checkNoDiagnosticEvents(session); - - checkCompleteEvent(session, 1, expectedSequenceId); - - session.clearMessages(); - }); - }); } diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index f30e8ad5d9c..996b81d4c2d 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -3653,52 +3653,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem watching @types", () => { - it("works correctly when typings are added or removed", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1;" - }; - const t1 = { - path: "/a/b/node_modules/@types/lib1/index.d.ts", - content: "export let a: number" - }; - const t2 = { - path: "/a/b/node_modules/@types/lib2/index.d.ts", - content: "export let b: number" - }; - const tsconfig = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - compilerOptions: {}, - exclude: ["node_modules"] - }) - }; - const host = createServerHost([f1, t1, tsconfig]); - const projectService = createProjectService(host); - - projectService.openClientFile(f1.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, t1.path, tsconfig.path]); - - // delete t1 - host.reloadFS([f1, tsconfig]); - // run throttled operation - host.runQueuedTimeoutCallbacks(); - - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]); - - // create t2 - host.reloadFS([f1, tsconfig, t2]); - // run throttled operation - host.runQueuedTimeoutCallbacks(); - - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, t2.path, tsconfig.path]); - }); - }); - describe("tsserverProjectSystem Open-file", () => { it("can be reloaded with empty content", () => { const f = { From 9e17a66151ac9b4c6bb9d6507dfbdecd3985d844 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 13:44:01 -0800 Subject: [PATCH 15/20] Sym links into single test --- src/testRunner/tsconfig.json | 1 + src/testRunner/unittests/tsserverHelpers.ts | 17 +- .../unittests/tsserverProjectSystem.ts | 289 ------------------ src/testRunner/unittests/tsserverSymLinks.ts | 282 +++++++++++++++++ 4 files changed, 292 insertions(+), 297 deletions(-) create mode 100644 src/testRunner/unittests/tsserverSymLinks.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index daad5a29b6a..8a5aab050c7 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -93,6 +93,7 @@ "unittests/tsserverProjectLoadingEvents.ts", "unittests/tsserverProjectSystem.ts", "unittests/tsserverProjectUpdatedInBackgroundEvent.ts", + "unittests/tsserverSymLinks.ts", "unittests/typingsInstaller.ts", "unittests/versionCache.ts", "unittests/watchEnvironment.ts", diff --git a/src/testRunner/unittests/tsserverHelpers.ts b/src/testRunner/unittests/tsserverHelpers.ts index f042a43738a..86dfddf3bb3 100644 --- a/src/testRunner/unittests/tsserverHelpers.ts +++ b/src/testRunner/unittests/tsserverHelpers.ts @@ -482,14 +482,15 @@ namespace ts.projectSystem { const toLocation = protocolToLocation(str); return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) }; } - //function protocolRenameSpanFromSubstring( - // str: string, - // substring: string, - // options?: SpanFromSubstringOptions, - // prefixSuffixText?: { readonly prefixText?: string, readonly suffixText?: string }, - //): protocol.RenameTextSpan { - // return { ...protocolTextSpanFromSubstring(str, substring, options), ...prefixSuffixText }; - //} + + export function protocolRenameSpanFromSubstring( + str: string, + substring: string, + options?: SpanFromSubstringOptions, + prefixSuffixText?: { readonly prefixText?: string, readonly suffixText?: string }, + ): protocol.RenameTextSpan { + return { ...protocolTextSpanFromSubstring(str, substring, options), ...prefixSuffixText }; + } export function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan { const start = nthIndexOf(str, substring, options ? options.index : 0); diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 996b81d4c2d..56493c01e35 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -7,14 +7,6 @@ namespace ts.projectSystem { checkArray("ScriptInfos files", arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles); } - function protocolRenameSpanFromSubstring( - str: string, - substring: string, - options?: SpanFromSubstringOptions, - prefixSuffixText?: { readonly prefixText?: string, readonly suffixText?: string }, - ): protocol.RenameTextSpan { - return { ...protocolTextSpanFromSubstring(str, substring, options), ...prefixSuffixText }; - } function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs { return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) }; } @@ -5144,287 +5136,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem with symLinks", () => { - it("rename in common file renames all project", () => { - const projects = "/users/username/projects"; - const folderA = `${projects}/a`; - const aFile: File = { - path: `${folderA}/a.ts`, - content: `import {C} from "./c/fc"; console.log(C)` - }; - const aTsconfig: File = { - path: `${folderA}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { module: "commonjs" } }) - }; - const aC: SymLink = { - path: `${folderA}/c`, - symLink: "../c" - }; - const aFc = `${folderA}/c/fc.ts`; - - const folderB = `${projects}/b`; - const bFile: File = { - path: `${folderB}/b.ts`, - content: `import {C} from "./c/fc"; console.log(C)` - }; - const bTsconfig: File = { - path: `${folderB}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { module: "commonjs" } }) - }; - const bC: SymLink = { - path: `${folderB}/c`, - symLink: "../c" - }; - const bFc = `${folderB}/c/fc.ts`; - - const folderC = `${projects}/c`; - const cFile: File = { - path: `${folderC}/fc.ts`, - content: `export const C = 8` - }; - - const files = [cFile, libFile, aFile, aTsconfig, aC, bFile, bTsconfig, bC]; - const host = createServerHost(files); - const session = createSession(host); - const projectService = session.getProjectService(); - openFilesForSession( - [ - { file: aFile, projectRootPath: folderA }, - { file: bFile, projectRootPath: folderB }, - { file: aFc, projectRootPath: folderA }, - { file: bFc, projectRootPath: folderB }, - ], - session); - checkNumberOfProjects(projectService, { configuredProjects: 2 }); - assert.isDefined(projectService.configuredProjects.get(aTsconfig.path)); - assert.isDefined(projectService.configuredProjects.get(bTsconfig.path)); - - const response = executeSessionRequest(session, protocol.CommandTypes.Rename, { file: aFc, ...protocolLocationFromSubstring(cFile.content, "C") }); - - assert.equal(aFile.content, bFile.content); - const abLocs: protocol.RenameTextSpan[] = [ - protocolRenameSpanFromSubstring(aFile.content, "C"), - protocolRenameSpanFromSubstring(aFile.content, "C", { index: 1 }), - ]; - const span = protocolRenameSpanFromSubstring(cFile.content, "C"); - const cLocs: protocol.RenameTextSpan[] = [span]; - assert.deepEqual(response, { - info: { - canRename: true, - displayName: "C", - fileToRename: undefined, - fullDisplayName: '"/users/username/projects/a/c/fc".C', - kind: ScriptElementKind.constElement, - kindModifiers: ScriptElementKindModifier.exportedModifier, - triggerSpan: protocolTextSpanFromSubstring(cFile.content, "C"), - }, - locs: [ - { file: aFc, locs: cLocs }, - { file: aFile.path, locs: abLocs }, - { file: bFc, locs: cLocs }, - { file: bFile.path, locs: abLocs }, - ], - }); - }); - - describe("module resolution when symlinked folder contents change and resolve modules", () => { - const projectRootPath = "/users/username/projects/myproject"; - const packages = `${projectRootPath}/javascript/packages`; - const recognizersDateTime = `${packages}/recognizers-date-time`; - const recognizersText = `${packages}/recognizers-text`; - const recognizersTextDist = `${recognizersText}/dist`; - const moduleName = "@microsoft/recognizers-text"; - const moduleNameInFile = `"${moduleName}"`; - const recognizersDateTimeSrcFile: File = { - path: `${recognizersDateTime}/src/datetime/baseDate.ts`, - content: `import {C} from ${moduleNameInFile}; -new C();` - }; - const recognizerDateTimeTsconfigPath = `${recognizersDateTime}/tsconfig.json`; - const recognizerDateTimeTsconfigWithoutPathMapping: File = { - path: recognizerDateTimeTsconfigPath, - content: JSON.stringify({ - include: ["src"] - }) - }; - const recognizerDateTimeTsconfigWithPathMapping: File = { - path: recognizerDateTimeTsconfigPath, - content: JSON.stringify({ - compilerOptions: { - rootDir: "src", - baseUrl: "./", - paths: { - "@microsoft/*": ["../*"] - } - }, - include: ["src"] - }) - }; - const nodeModulesRecorgnizersText: SymLink = { - path: `${recognizersDateTime}/node_modules/@microsoft/recognizers-text`, - symLink: recognizersText - }; - const recognizerTextSrcFile: File = { - path: `${recognizersText}/src/recognizers-text.ts`, - content: `export class C { method () { return 10; } }` - }; - const recongnizerTextDistTypingFile: File = { - path: `${recognizersTextDist}/types/recognizers-text.d.ts`, - content: `export class C { method(): number; }` - }; - const recongnizerTextPackageJson: File = { - path: `${recognizersText}/package.json`, - content: JSON.stringify({ - typings: "dist/types/recognizers-text.d.ts" - }) - }; - const filesInProjectWithUnresolvedModule = [recognizerDateTimeTsconfigPath, libFile.path, recognizersDateTimeSrcFile.path]; - const filesInProjectWithResolvedModule = [...filesInProjectWithUnresolvedModule, recongnizerTextDistTypingFile.path]; - - function verifyErrors(session: TestSession, semanticErrors: protocol.Diagnostic[]) { - session.clearMessages(); - const expectedSequenceId = session.getNextSeq(); - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [recognizersDateTimeSrcFile.path], - } - }); - - const host = session.host; - host.checkTimeoutQueueLengthAndRun(1); - - checkErrorMessage(session, "syntaxDiag", { file: recognizersDateTimeSrcFile.path, diagnostics: [] }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - - checkErrorMessage(session, "semanticDiag", { file: recognizersDateTimeSrcFile.path, diagnostics: semanticErrors }); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - - checkErrorMessage(session, "suggestionDiag", { - file: recognizersDateTimeSrcFile.path, - diagnostics: [], - }); - checkCompleteEvent(session, 2, expectedSequenceId); - } - - function verifyWatchedFilesAndDirectories(host: TestServerHost, files: string[], recursiveDirectories: ReadonlyMap, nonRecursiveDirectories: string[]) { - checkWatchedFilesDetailed(host, files.filter(f => f !== recognizersDateTimeSrcFile.path), 1); - checkWatchedDirectoriesDetailed(host, nonRecursiveDirectories, 1, /*recursive*/ false); - checkWatchedDirectoriesDetailed(host, recursiveDirectories, /*recursive*/ true); - } - - function createSessionAndOpenFile(host: TestServerHost) { - const session = createSession(host, { canUseEvents: true }); - session.executeCommandSeq({ - command: protocol.CommandTypes.Open, - arguments: { - file: recognizersDateTimeSrcFile.path, - projectRootPath - } - }); - return session; - } - - function verifyModuleResolution(withPathMapping: boolean) { - describe(withPathMapping ? "when tsconfig file contains path mapping" : "when tsconfig does not contain path mapping", () => { - const filesWithSources = [libFile, recognizersDateTimeSrcFile, withPathMapping ? recognizerDateTimeTsconfigWithPathMapping : recognizerDateTimeTsconfigWithoutPathMapping, recognizerTextSrcFile, recongnizerTextPackageJson]; - const filesWithNodeModulesSetup = [...filesWithSources, nodeModulesRecorgnizersText]; - const filesAfterCompilation = [...filesWithNodeModulesSetup, recongnizerTextDistTypingFile]; - - const watchedDirectoriesWithResolvedModule = arrayToMap(getTypeRootsFromLocation(recognizersDateTime), k => k, () => 1); - watchedDirectoriesWithResolvedModule.set(`${recognizersDateTime}/src`, withPathMapping ? 1 : 2); // wild card + failed lookups - if (!withPathMapping) { - watchedDirectoriesWithResolvedModule.set(`${recognizersDateTime}/node_modules`, 1); // failed lookups - } - const watchedDirectoriesWithUnresolvedModule = cloneMap(watchedDirectoriesWithResolvedModule); - watchedDirectoriesWithUnresolvedModule.set(`${recognizersDateTime}/src`, 2); // wild card + failed lookups - [`${recognizersDateTime}/node_modules`, ...(withPathMapping ? [recognizersText] : emptyArray), ...getNodeModuleDirectories(packages)].forEach(d => { - watchedDirectoriesWithUnresolvedModule.set(d, 1); - }); - const nonRecursiveWatchedDirectories = withPathMapping ? [packages] : emptyArray; - - function verifyProjectWithResolvedModule(session: TestSession) { - const projectService = session.getProjectService(); - const project = projectService.configuredProjects.get(recognizerDateTimeTsconfigPath)!; - checkProjectActualFiles(project, filesInProjectWithResolvedModule); - verifyWatchedFilesAndDirectories(session.host, filesInProjectWithResolvedModule, watchedDirectoriesWithResolvedModule, nonRecursiveWatchedDirectories); - verifyErrors(session, []); - } - - function verifyProjectWithUnresolvedModule(session: TestSession) { - const projectService = session.getProjectService(); - const project = projectService.configuredProjects.get(recognizerDateTimeTsconfigPath)!; - checkProjectActualFiles(project, filesInProjectWithUnresolvedModule); - verifyWatchedFilesAndDirectories(session.host, filesInProjectWithUnresolvedModule, watchedDirectoriesWithUnresolvedModule, nonRecursiveWatchedDirectories); - const startOffset = recognizersDateTimeSrcFile.content.indexOf('"') + 1; - verifyErrors(session, [ - createDiagnostic({ line: 1, offset: startOffset }, { line: 1, offset: startOffset + moduleNameInFile.length }, Diagnostics.Cannot_find_module_0, [moduleName]) - ]); - } - - it("when project compiles from sources", () => { - const host = createServerHost(filesWithSources); - const session = createSessionAndOpenFile(host); - verifyProjectWithUnresolvedModule(session); - - host.reloadFS(filesAfterCompilation); - host.runQueuedTimeoutCallbacks(); - - verifyProjectWithResolvedModule(session); - }); - - it("when project has node_modules setup but doesnt have modules in typings folder and then recompiles", () => { - const host = createServerHost(filesWithNodeModulesSetup); - const session = createSessionAndOpenFile(host); - verifyProjectWithUnresolvedModule(session); - - host.reloadFS(filesAfterCompilation); - host.runQueuedTimeoutCallbacks(); - - if (withPathMapping) { - verifyProjectWithResolvedModule(session); - } - else { - // Cannot handle the resolution update - verifyProjectWithUnresolvedModule(session); - } - }); - - it("when project recompiles after deleting generated folders", () => { - const host = createServerHost(filesAfterCompilation); - const session = createSessionAndOpenFile(host); - - verifyProjectWithResolvedModule(session); - - host.deleteFolder(recognizersTextDist, /*recursive*/ true); - host.runQueuedTimeoutCallbacks(); - - verifyProjectWithUnresolvedModule(session); - - host.ensureFileOrFolder(recongnizerTextDistTypingFile); - host.runQueuedTimeoutCallbacks(); - - if (withPathMapping) { - verifyProjectWithResolvedModule(session); - } - else { - // Cannot handle the resolution update - verifyProjectWithUnresolvedModule(session); - } - }); - }); - } - - verifyModuleResolution(/*withPathMapping*/ false); - verifyModuleResolution(/*withPathMapping*/ true); - }); - }); - describe("tsserverProjectSystem forceConsistentCasingInFileNames", () => { it("works when extends is specified with a case insensitive file system", () => { const rootPath = "/Users/username/dev/project"; diff --git a/src/testRunner/unittests/tsserverSymLinks.ts b/src/testRunner/unittests/tsserverSymLinks.ts new file mode 100644 index 00000000000..c9bad7ea678 --- /dev/null +++ b/src/testRunner/unittests/tsserverSymLinks.ts @@ -0,0 +1,282 @@ +namespace ts.projectSystem { + describe("tsserverProjectSystem with symLinks", () => { + it("rename in common file renames all project", () => { + const projects = "/users/username/projects"; + const folderA = `${projects}/a`; + const aFile: File = { + path: `${folderA}/a.ts`, + content: `import {C} from "./c/fc"; console.log(C)` + }; + const aTsconfig: File = { + path: `${folderA}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { module: "commonjs" } }) + }; + const aC: SymLink = { + path: `${folderA}/c`, + symLink: "../c" + }; + const aFc = `${folderA}/c/fc.ts`; + + const folderB = `${projects}/b`; + const bFile: File = { + path: `${folderB}/b.ts`, + content: `import {C} from "./c/fc"; console.log(C)` + }; + const bTsconfig: File = { + path: `${folderB}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { module: "commonjs" } }) + }; + const bC: SymLink = { + path: `${folderB}/c`, + symLink: "../c" + }; + const bFc = `${folderB}/c/fc.ts`; + + const folderC = `${projects}/c`; + const cFile: File = { + path: `${folderC}/fc.ts`, + content: `export const C = 8` + }; + + const files = [cFile, libFile, aFile, aTsconfig, aC, bFile, bTsconfig, bC]; + const host = createServerHost(files); + const session = createSession(host); + const projectService = session.getProjectService(); + openFilesForSession( + [ + { file: aFile, projectRootPath: folderA }, + { file: bFile, projectRootPath: folderB }, + { file: aFc, projectRootPath: folderA }, + { file: bFc, projectRootPath: folderB }, + ], + session); + checkNumberOfProjects(projectService, { configuredProjects: 2 }); + assert.isDefined(projectService.configuredProjects.get(aTsconfig.path)); + assert.isDefined(projectService.configuredProjects.get(bTsconfig.path)); + + const response = executeSessionRequest(session, protocol.CommandTypes.Rename, { file: aFc, ...protocolLocationFromSubstring(cFile.content, "C") }); + + assert.equal(aFile.content, bFile.content); + const abLocs: protocol.RenameTextSpan[] = [ + protocolRenameSpanFromSubstring(aFile.content, "C"), + protocolRenameSpanFromSubstring(aFile.content, "C", { index: 1 }), + ]; + const span = protocolRenameSpanFromSubstring(cFile.content, "C"); + const cLocs: protocol.RenameTextSpan[] = [span]; + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "C", + fileToRename: undefined, + fullDisplayName: '"/users/username/projects/a/c/fc".C', + kind: ScriptElementKind.constElement, + kindModifiers: ScriptElementKindModifier.exportedModifier, + triggerSpan: protocolTextSpanFromSubstring(cFile.content, "C"), + }, + locs: [ + { file: aFc, locs: cLocs }, + { file: aFile.path, locs: abLocs }, + { file: bFc, locs: cLocs }, + { file: bFile.path, locs: abLocs }, + ], + }); + }); + + describe("module resolution when symlinked folder contents change and resolve modules", () => { + const projectRootPath = "/users/username/projects/myproject"; + const packages = `${projectRootPath}/javascript/packages`; + const recognizersDateTime = `${packages}/recognizers-date-time`; + const recognizersText = `${packages}/recognizers-text`; + const recognizersTextDist = `${recognizersText}/dist`; + const moduleName = "@microsoft/recognizers-text"; + const moduleNameInFile = `"${moduleName}"`; + const recognizersDateTimeSrcFile: File = { + path: `${recognizersDateTime}/src/datetime/baseDate.ts`, + content: `import {C} from ${moduleNameInFile}; +new C();` + }; + const recognizerDateTimeTsconfigPath = `${recognizersDateTime}/tsconfig.json`; + const recognizerDateTimeTsconfigWithoutPathMapping: File = { + path: recognizerDateTimeTsconfigPath, + content: JSON.stringify({ + include: ["src"] + }) + }; + const recognizerDateTimeTsconfigWithPathMapping: File = { + path: recognizerDateTimeTsconfigPath, + content: JSON.stringify({ + compilerOptions: { + rootDir: "src", + baseUrl: "./", + paths: { + "@microsoft/*": ["../*"] + } + }, + include: ["src"] + }) + }; + const nodeModulesRecorgnizersText: SymLink = { + path: `${recognizersDateTime}/node_modules/@microsoft/recognizers-text`, + symLink: recognizersText + }; + const recognizerTextSrcFile: File = { + path: `${recognizersText}/src/recognizers-text.ts`, + content: `export class C { method () { return 10; } }` + }; + const recongnizerTextDistTypingFile: File = { + path: `${recognizersTextDist}/types/recognizers-text.d.ts`, + content: `export class C { method(): number; }` + }; + const recongnizerTextPackageJson: File = { + path: `${recognizersText}/package.json`, + content: JSON.stringify({ + typings: "dist/types/recognizers-text.d.ts" + }) + }; + const filesInProjectWithUnresolvedModule = [recognizerDateTimeTsconfigPath, libFile.path, recognizersDateTimeSrcFile.path]; + const filesInProjectWithResolvedModule = [...filesInProjectWithUnresolvedModule, recongnizerTextDistTypingFile.path]; + + function verifyErrors(session: TestSession, semanticErrors: protocol.Diagnostic[]) { + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [recognizersDateTimeSrcFile.path], + } + }); + + const host = session.host; + host.checkTimeoutQueueLengthAndRun(1); + + checkErrorMessage(session, "syntaxDiag", { file: recognizersDateTimeSrcFile.path, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + + checkErrorMessage(session, "semanticDiag", { file: recognizersDateTimeSrcFile.path, diagnostics: semanticErrors }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + + checkErrorMessage(session, "suggestionDiag", { + file: recognizersDateTimeSrcFile.path, + diagnostics: [], + }); + checkCompleteEvent(session, 2, expectedSequenceId); + } + + function verifyWatchedFilesAndDirectories(host: TestServerHost, files: string[], recursiveDirectories: ReadonlyMap, nonRecursiveDirectories: string[]) { + checkWatchedFilesDetailed(host, files.filter(f => f !== recognizersDateTimeSrcFile.path), 1); + checkWatchedDirectoriesDetailed(host, nonRecursiveDirectories, 1, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, recursiveDirectories, /*recursive*/ true); + } + + function createSessionAndOpenFile(host: TestServerHost) { + const session = createSession(host, { canUseEvents: true }); + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { + file: recognizersDateTimeSrcFile.path, + projectRootPath + } + }); + return session; + } + + function verifyModuleResolution(withPathMapping: boolean) { + describe(withPathMapping ? "when tsconfig file contains path mapping" : "when tsconfig does not contain path mapping", () => { + const filesWithSources = [libFile, recognizersDateTimeSrcFile, withPathMapping ? recognizerDateTimeTsconfigWithPathMapping : recognizerDateTimeTsconfigWithoutPathMapping, recognizerTextSrcFile, recongnizerTextPackageJson]; + const filesWithNodeModulesSetup = [...filesWithSources, nodeModulesRecorgnizersText]; + const filesAfterCompilation = [...filesWithNodeModulesSetup, recongnizerTextDistTypingFile]; + + const watchedDirectoriesWithResolvedModule = arrayToMap(getTypeRootsFromLocation(recognizersDateTime), k => k, () => 1); + watchedDirectoriesWithResolvedModule.set(`${recognizersDateTime}/src`, withPathMapping ? 1 : 2); // wild card + failed lookups + if (!withPathMapping) { + watchedDirectoriesWithResolvedModule.set(`${recognizersDateTime}/node_modules`, 1); // failed lookups + } + const watchedDirectoriesWithUnresolvedModule = cloneMap(watchedDirectoriesWithResolvedModule); + watchedDirectoriesWithUnresolvedModule.set(`${recognizersDateTime}/src`, 2); // wild card + failed lookups + [`${recognizersDateTime}/node_modules`, ...(withPathMapping ? [recognizersText] : emptyArray), ...getNodeModuleDirectories(packages)].forEach(d => { + watchedDirectoriesWithUnresolvedModule.set(d, 1); + }); + const nonRecursiveWatchedDirectories = withPathMapping ? [packages] : emptyArray; + + function verifyProjectWithResolvedModule(session: TestSession) { + const projectService = session.getProjectService(); + const project = projectService.configuredProjects.get(recognizerDateTimeTsconfigPath)!; + checkProjectActualFiles(project, filesInProjectWithResolvedModule); + verifyWatchedFilesAndDirectories(session.host, filesInProjectWithResolvedModule, watchedDirectoriesWithResolvedModule, nonRecursiveWatchedDirectories); + verifyErrors(session, []); + } + + function verifyProjectWithUnresolvedModule(session: TestSession) { + const projectService = session.getProjectService(); + const project = projectService.configuredProjects.get(recognizerDateTimeTsconfigPath)!; + checkProjectActualFiles(project, filesInProjectWithUnresolvedModule); + verifyWatchedFilesAndDirectories(session.host, filesInProjectWithUnresolvedModule, watchedDirectoriesWithUnresolvedModule, nonRecursiveWatchedDirectories); + const startOffset = recognizersDateTimeSrcFile.content.indexOf('"') + 1; + verifyErrors(session, [ + createDiagnostic({ line: 1, offset: startOffset }, { line: 1, offset: startOffset + moduleNameInFile.length }, Diagnostics.Cannot_find_module_0, [moduleName]) + ]); + } + + it("when project compiles from sources", () => { + const host = createServerHost(filesWithSources); + const session = createSessionAndOpenFile(host); + verifyProjectWithUnresolvedModule(session); + + host.reloadFS(filesAfterCompilation); + host.runQueuedTimeoutCallbacks(); + + verifyProjectWithResolvedModule(session); + }); + + it("when project has node_modules setup but doesnt have modules in typings folder and then recompiles", () => { + const host = createServerHost(filesWithNodeModulesSetup); + const session = createSessionAndOpenFile(host); + verifyProjectWithUnresolvedModule(session); + + host.reloadFS(filesAfterCompilation); + host.runQueuedTimeoutCallbacks(); + + if (withPathMapping) { + verifyProjectWithResolvedModule(session); + } + else { + // Cannot handle the resolution update + verifyProjectWithUnresolvedModule(session); + } + }); + + it("when project recompiles after deleting generated folders", () => { + const host = createServerHost(filesAfterCompilation); + const session = createSessionAndOpenFile(host); + + verifyProjectWithResolvedModule(session); + + host.deleteFolder(recognizersTextDist, /*recursive*/ true); + host.runQueuedTimeoutCallbacks(); + + verifyProjectWithUnresolvedModule(session); + + host.ensureFileOrFolder(recongnizerTextDistTypingFile); + host.runQueuedTimeoutCallbacks(); + + if (withPathMapping) { + verifyProjectWithResolvedModule(session); + } + else { + // Cannot handle the resolution update + verifyProjectWithUnresolvedModule(session); + } + }); + }); + } + + verifyModuleResolution(/*withPathMapping*/ false); + verifyModuleResolution(/*withPathMapping*/ true); + }); + }); +} From 5c8ef3934d81d77cc9f780419aa8b39128e313c9 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 13:53:51 -0800 Subject: [PATCH 16/20] Move the tests into their own folder for easy scenario search --- src/testRunner/tsconfig.json | 83 ++- .../{ => config}/commandLineParsing.ts | 0 .../{ => config}/configurationExtension.ts | 0 .../convertCompilerOptionsFromJson.ts | 0 .../convertTypeAcquisitionFromJson.ts | 0 .../{ => config}/initializeTSConfig.ts | 0 .../unittests/{ => config}/matchFiles.ts | 0 .../{ => config}/projectReferences.ts | 0 .../unittests/{ => config}/showConfig.ts | 0 .../unittests/{ => config}/tsconfigParsing.ts | 0 .../cancellableLanguageServiceOperations.ts | 0 .../{ => services}/convertToAsyncFunction.ts | 0 .../extract/constants.ts} | 0 .../extract/functions.ts} | 0 .../extract/helpers.ts} | 0 .../extract/ranges.ts} | 0 .../{ => services/extract}/symbolWalker.ts | 2 +- .../{ => services}/hostNewLineSupport.ts | 0 .../{ => services}/languageService.ts | 0 .../{ => services}/organizeImports.ts | 0 .../unittests/{ => services}/textChanges.ts | 0 .../{tscWatchEmit.ts => tscWatch/emit.ts} | 0 .../helpers.ts} | 0 .../unittests/tscWatch/resolutionCache.ts | 448 ++++++++++++ .../unittests/{ => tscWatch}/watchApi.ts | 0 .../unittests/tscWatch/watchEnvironment.ts | 178 +++++ .../cachingFileSystemInformation.ts} | 2 +- .../unittests/{ => tsserver}/compileOnSave.ts | 4 +- .../events/largeFileReferenced.ts} | 2 +- .../events/projectLoading.ts} | 2 +- .../events/projectUpdatedInBackground.ts} | 2 +- .../unittests/tsserver/externalProjects.ts | 300 ++++++++ .../helpers.ts} | 0 .../unittests/{ => tsserver}/projectErrors.ts | 0 src/testRunner/unittests/tsserver/reload.ts | 151 ++++ .../{ => tsserver}/resolutionCache.ts | 449 ------------ .../unittests/{ => tsserver}/session.ts | 0 .../unittests/tsserver/skipLibCheck.ts | 229 ++++++ .../symLinks.ts} | 2 +- .../unittests/{ => tsserver}/telemetry.ts | 0 .../unittests/{ => tsserver}/textStorage.ts | 0 .../{ => tsserver}/typingsInstaller.ts | 0 .../unittests/{ => tsserver}/versionCache.ts | 0 .../unittests/tsserver/watchEnvironment.ts | 135 ++++ .../unittests/tsserverProjectSystem.ts | 677 ------------------ src/testRunner/unittests/watchEnvironment.ts | 316 -------- 46 files changed, 1493 insertions(+), 1489 deletions(-) rename src/testRunner/unittests/{ => config}/commandLineParsing.ts (100%) rename src/testRunner/unittests/{ => config}/configurationExtension.ts (100%) rename src/testRunner/unittests/{ => config}/convertCompilerOptionsFromJson.ts (100%) rename src/testRunner/unittests/{ => config}/convertTypeAcquisitionFromJson.ts (100%) rename src/testRunner/unittests/{ => config}/initializeTSConfig.ts (100%) rename src/testRunner/unittests/{ => config}/matchFiles.ts (100%) rename src/testRunner/unittests/{ => config}/projectReferences.ts (100%) rename src/testRunner/unittests/{ => config}/showConfig.ts (100%) rename src/testRunner/unittests/{ => config}/tsconfigParsing.ts (100%) rename src/testRunner/unittests/{ => services}/cancellableLanguageServiceOperations.ts (100%) rename src/testRunner/unittests/{ => services}/convertToAsyncFunction.ts (100%) rename src/testRunner/unittests/{extractConstants.ts => services/extract/constants.ts} (100%) rename src/testRunner/unittests/{extractFunctions.ts => services/extract/functions.ts} (100%) rename src/testRunner/unittests/{extractTestHelpers.ts => services/extract/helpers.ts} (100%) rename src/testRunner/unittests/{extractRanges.ts => services/extract/ranges.ts} (100%) rename src/testRunner/unittests/{ => services/extract}/symbolWalker.ts (97%) rename src/testRunner/unittests/{ => services}/hostNewLineSupport.ts (100%) rename src/testRunner/unittests/{ => services}/languageService.ts (100%) rename src/testRunner/unittests/{ => services}/organizeImports.ts (100%) rename src/testRunner/unittests/{ => services}/textChanges.ts (100%) rename src/testRunner/unittests/{tscWatchEmit.ts => tscWatch/emit.ts} (100%) rename src/testRunner/unittests/{tscWatchHelpers.ts => tscWatch/helpers.ts} (100%) create mode 100644 src/testRunner/unittests/tscWatch/resolutionCache.ts rename src/testRunner/unittests/{ => tscWatch}/watchApi.ts (100%) create mode 100644 src/testRunner/unittests/tscWatch/watchEnvironment.ts rename src/testRunner/unittests/{tsserverCachingFileSystemInformation.ts => tsserver/cachingFileSystemInformation.ts} (98%) rename src/testRunner/unittests/{ => tsserver}/compileOnSave.ts (97%) rename src/testRunner/unittests/{tsserverLargeFileReferencedEvent.ts => tsserver/events/largeFileReferenced.ts} (95%) rename src/testRunner/unittests/{tsserverProjectLoadingEvents.ts => tsserver/events/projectLoading.ts} (96%) rename src/testRunner/unittests/{tsserverProjectUpdatedInBackgroundEvent.ts => tsserver/events/projectUpdatedInBackground.ts} (97%) create mode 100644 src/testRunner/unittests/tsserver/externalProjects.ts rename src/testRunner/unittests/{tsserverHelpers.ts => tsserver/helpers.ts} (100%) rename src/testRunner/unittests/{ => tsserver}/projectErrors.ts (100%) create mode 100644 src/testRunner/unittests/tsserver/reload.ts rename src/testRunner/unittests/{ => tsserver}/resolutionCache.ts (71%) rename src/testRunner/unittests/{ => tsserver}/session.ts (100%) create mode 100644 src/testRunner/unittests/tsserver/skipLibCheck.ts rename src/testRunner/unittests/{tsserverSymLinks.ts => tsserver/symLinks.ts} (97%) rename src/testRunner/unittests/{ => tsserver}/telemetry.ts (100%) rename src/testRunner/unittests/{ => tsserver}/textStorage.ts (100%) rename src/testRunner/unittests/{ => tsserver}/typingsInstaller.ts (100%) rename src/testRunner/unittests/{ => tsserver}/versionCache.ts (100%) create mode 100644 src/testRunner/unittests/tsserver/watchEnvironment.ts delete mode 100644 src/testRunner/unittests/watchEnvironment.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 8a5aab050c7..a7d82b84464 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -36,74 +36,79 @@ "runner.ts", - "unittests/extractTestHelpers.ts", - "unittests/tscWatchHelpers.ts", - "unittests/tsserverHelpers.ts", + "unittests/services/extract/helpers.ts", + "unittests/tscWatch/helpers.ts", + "unittests/tsserver/helpers.ts", "unittests/asserts.ts", "unittests/base64.ts", "unittests/builder.ts", - "unittests/cancellableLanguageServiceOperations.ts", - "unittests/commandLineParsing.ts", - "unittests/compileOnSave.ts", "unittests/compilerCore.ts", - "unittests/configurationExtension.ts", - "unittests/convertCompilerOptionsFromJson.ts", - "unittests/convertToAsyncFunction.ts", "unittests/convertToBase64.ts", - "unittests/convertTypeAcquisitionFromJson.ts", "unittests/customTransforms.ts", - "unittests/extractConstants.ts", - "unittests/extractFunctions.ts", - "unittests/extractRanges.ts", "unittests/factory.ts", - "unittests/hostNewLineSupport.ts", "unittests/incrementalParser.ts", - "unittests/initializeTSConfig.ts", "unittests/jsDocParsing.ts", - "unittests/languageService.ts", - "unittests/matchFiles.ts", "unittests/moduleResolution.ts", - "unittests/organizeImports.ts", "unittests/parsePseudoBigInt.ts", "unittests/paths.ts", "unittests/printer.ts", "unittests/programApi.ts", - "unittests/projectErrors.ts", - "unittests/projectReferences.ts", "unittests/publicApi.ts", - "unittests/resolutionCache.ts", "unittests/reuseProgramStructure.ts", - "unittests/session.ts", "unittests/semver.ts", - "unittests/showConfig.ts", - "unittests/symbolWalker.ts", - "unittests/telemetry.ts", - "unittests/textChanges.ts", - "unittests/textStorage.ts", "unittests/transform.ts", "unittests/transpile.ts", "unittests/tsbuild.ts", "unittests/tsbuildWatchMode.ts", - "unittests/tsconfigParsing.ts", - "unittests/tscWatchEmit.ts", "unittests/tscWatchMode.ts", - "unittests/tsserverCachingFileSystemInformation.ts", - "unittests/tsserverLargeFileReferencedEvent.ts", - "unittests/tsserverProjectLoadingEvents.ts", "unittests/tsserverProjectSystem.ts", - "unittests/tsserverProjectUpdatedInBackgroundEvent.ts", - "unittests/tsserverSymLinks.ts", - "unittests/typingsInstaller.ts", - "unittests/versionCache.ts", - "unittests/watchEnvironment.ts", - "unittests/watchApi.ts", + "unittests/config/commandLineParsing.ts", + "unittests/config/configurationExtension.ts", + "unittests/config/convertCompilerOptionsFromJson.ts", + "unittests/config/convertTypeAcquisitionFromJson.ts", + "unittests/config/initializeTSConfig.ts", + "unittests/config/matchFiles.ts", + "unittests/config/projectReferences.ts", + "unittests/config/showConfig.ts", + "unittests/config/tsconfigParsing.ts", "unittests/evaluation/asyncArrow.ts", "unittests/evaluation/asyncGenerator.ts", "unittests/evaluation/forAwaitOf.ts", + "unittests/services/cancellableLanguageServiceOperations.ts", "unittests/services/colorization.ts", + "unittests/services/convertToAsyncFunction.ts", "unittests/services/documentRegistry.ts", + "unittests/services/extract/constants.ts", + "unittests/services/extract/functions.ts", + "unittests/services/extract/symbolWalker.ts", + "unittests/services/extract/ranges.ts", + "unittests/services/hostNewLineSupport.ts", + "unittests/services/languageService.ts", + "unittests/services/organizeImports.ts", "unittests/services/patternMatcher.ts", - "unittests/services/preProcessFile.ts" + "unittests/services/preProcessFile.ts", + "unittests/services/textChanges.ts", + "unittests/tscWatch/emit.ts", + "unittests/tscWatch/resolutionCache.ts", + "unittests/tscWatch/watchEnvironment.ts", + "unittests/tscWatch/watchApi.ts", + "unittests/tsserver/cachingFileSystemInformation.ts", + "unittests/tsserver/compileOnSave.ts", + "unittests/tsserver/events/largeFileReferenced.ts", + "unittests/tsserver/events/projectLoading.ts", + "unittests/tsserver/events/projectUpdatedInBackground.ts", + "unittests/tsserver/externalProjects.ts", + "unittests/tsserver/projectErrors.ts", + "unittests/tsserver/reload.ts", + "unittests/tsserver/resolutionCache.ts", + "unittests/tsserver/session.ts", + "unittests/tsserver/skipLibCheck.ts", + "unittests/tsserver/symLinks.ts", + "unittests/tsserver/textStorage.ts", + "unittests/tsserver/telemetry.ts", + "unittests/tsserver/typingsInstaller.ts", + "unittests/tsserver/versionCache.ts", + "unittests/tsserver/watchEnvironment.ts" ] } diff --git a/src/testRunner/unittests/commandLineParsing.ts b/src/testRunner/unittests/config/commandLineParsing.ts similarity index 100% rename from src/testRunner/unittests/commandLineParsing.ts rename to src/testRunner/unittests/config/commandLineParsing.ts diff --git a/src/testRunner/unittests/configurationExtension.ts b/src/testRunner/unittests/config/configurationExtension.ts similarity index 100% rename from src/testRunner/unittests/configurationExtension.ts rename to src/testRunner/unittests/config/configurationExtension.ts diff --git a/src/testRunner/unittests/convertCompilerOptionsFromJson.ts b/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts similarity index 100% rename from src/testRunner/unittests/convertCompilerOptionsFromJson.ts rename to src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts diff --git a/src/testRunner/unittests/convertTypeAcquisitionFromJson.ts b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts similarity index 100% rename from src/testRunner/unittests/convertTypeAcquisitionFromJson.ts rename to src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts diff --git a/src/testRunner/unittests/initializeTSConfig.ts b/src/testRunner/unittests/config/initializeTSConfig.ts similarity index 100% rename from src/testRunner/unittests/initializeTSConfig.ts rename to src/testRunner/unittests/config/initializeTSConfig.ts diff --git a/src/testRunner/unittests/matchFiles.ts b/src/testRunner/unittests/config/matchFiles.ts similarity index 100% rename from src/testRunner/unittests/matchFiles.ts rename to src/testRunner/unittests/config/matchFiles.ts diff --git a/src/testRunner/unittests/projectReferences.ts b/src/testRunner/unittests/config/projectReferences.ts similarity index 100% rename from src/testRunner/unittests/projectReferences.ts rename to src/testRunner/unittests/config/projectReferences.ts diff --git a/src/testRunner/unittests/showConfig.ts b/src/testRunner/unittests/config/showConfig.ts similarity index 100% rename from src/testRunner/unittests/showConfig.ts rename to src/testRunner/unittests/config/showConfig.ts diff --git a/src/testRunner/unittests/tsconfigParsing.ts b/src/testRunner/unittests/config/tsconfigParsing.ts similarity index 100% rename from src/testRunner/unittests/tsconfigParsing.ts rename to src/testRunner/unittests/config/tsconfigParsing.ts diff --git a/src/testRunner/unittests/cancellableLanguageServiceOperations.ts b/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts similarity index 100% rename from src/testRunner/unittests/cancellableLanguageServiceOperations.ts rename to src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts diff --git a/src/testRunner/unittests/convertToAsyncFunction.ts b/src/testRunner/unittests/services/convertToAsyncFunction.ts similarity index 100% rename from src/testRunner/unittests/convertToAsyncFunction.ts rename to src/testRunner/unittests/services/convertToAsyncFunction.ts diff --git a/src/testRunner/unittests/extractConstants.ts b/src/testRunner/unittests/services/extract/constants.ts similarity index 100% rename from src/testRunner/unittests/extractConstants.ts rename to src/testRunner/unittests/services/extract/constants.ts diff --git a/src/testRunner/unittests/extractFunctions.ts b/src/testRunner/unittests/services/extract/functions.ts similarity index 100% rename from src/testRunner/unittests/extractFunctions.ts rename to src/testRunner/unittests/services/extract/functions.ts diff --git a/src/testRunner/unittests/extractTestHelpers.ts b/src/testRunner/unittests/services/extract/helpers.ts similarity index 100% rename from src/testRunner/unittests/extractTestHelpers.ts rename to src/testRunner/unittests/services/extract/helpers.ts diff --git a/src/testRunner/unittests/extractRanges.ts b/src/testRunner/unittests/services/extract/ranges.ts similarity index 100% rename from src/testRunner/unittests/extractRanges.ts rename to src/testRunner/unittests/services/extract/ranges.ts diff --git a/src/testRunner/unittests/symbolWalker.ts b/src/testRunner/unittests/services/extract/symbolWalker.ts similarity index 97% rename from src/testRunner/unittests/symbolWalker.ts rename to src/testRunner/unittests/services/extract/symbolWalker.ts index 4743b87133b..a027f0f2ce7 100644 --- a/src/testRunner/unittests/symbolWalker.ts +++ b/src/testRunner/unittests/services/extract/symbolWalker.ts @@ -42,4 +42,4 @@ export default function foo(a: number, b: Bar): void {}`, (file, checker) => { assert.equal(stdLibRefSymbols, 1); // Expect 1 stdlib entry symbol - the implicit Array referenced by Bar.history }); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/hostNewLineSupport.ts b/src/testRunner/unittests/services/hostNewLineSupport.ts similarity index 100% rename from src/testRunner/unittests/hostNewLineSupport.ts rename to src/testRunner/unittests/services/hostNewLineSupport.ts diff --git a/src/testRunner/unittests/languageService.ts b/src/testRunner/unittests/services/languageService.ts similarity index 100% rename from src/testRunner/unittests/languageService.ts rename to src/testRunner/unittests/services/languageService.ts diff --git a/src/testRunner/unittests/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts similarity index 100% rename from src/testRunner/unittests/organizeImports.ts rename to src/testRunner/unittests/services/organizeImports.ts diff --git a/src/testRunner/unittests/textChanges.ts b/src/testRunner/unittests/services/textChanges.ts similarity index 100% rename from src/testRunner/unittests/textChanges.ts rename to src/testRunner/unittests/services/textChanges.ts diff --git a/src/testRunner/unittests/tscWatchEmit.ts b/src/testRunner/unittests/tscWatch/emit.ts similarity index 100% rename from src/testRunner/unittests/tscWatchEmit.ts rename to src/testRunner/unittests/tscWatch/emit.ts diff --git a/src/testRunner/unittests/tscWatchHelpers.ts b/src/testRunner/unittests/tscWatch/helpers.ts similarity index 100% rename from src/testRunner/unittests/tscWatchHelpers.ts rename to src/testRunner/unittests/tscWatch/helpers.ts diff --git a/src/testRunner/unittests/tscWatch/resolutionCache.ts b/src/testRunner/unittests/tscWatch/resolutionCache.ts new file mode 100644 index 00000000000..759c80e93a6 --- /dev/null +++ b/src/testRunner/unittests/tscWatch/resolutionCache.ts @@ -0,0 +1,448 @@ +namespace ts.tscWatch { + describe("resolutionCache:: tsc-watch module resolution caching", () => { + it("works", () => { + const root = { + path: "/a/d/f0.ts", + content: `import {x} from "f1"` + }; + const imported = { + path: "/a/f1.ts", + content: `foo()` + }; + + const files = [root, imported, libFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); + const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); + + // ensure that imported file was found + checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); + + const originalFileExists = host.fileExists; + { + const newContent = `import {x} from "f1" + var x: string = 1;`; + root.content = newContent; + host.reloadFS(files); + + // patch fileExists to make sure that disk is not touched + host.fileExists = notImplemented; + + // trigger synchronization to make sure that import will be fetched from the cache + host.runQueuedTimeoutCallbacks(); + + // ensure file has correct number of errors after edit + checkOutputErrorsIncremental(host, [ + f1IsNotModule, + getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), + cannotFindFoo + ]); + } + { + let fileExistsIsCalled = false; + host.fileExists = (fileName): boolean => { + if (fileName === "lib.d.ts") { + return false; + } + fileExistsIsCalled = true; + assert.isTrue(fileName.indexOf("/f2.") !== -1); + return originalFileExists.call(host, fileName); + }; + + root.content = `import {x} from "f2"`; + host.reloadFS(files); + + // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + host.runQueuedTimeoutCallbacks(); + + // ensure file has correct number of errors after edit + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") + ]); + + assert.isTrue(fileExistsIsCalled); + } + { + let fileExistsCalled = false; + host.fileExists = (fileName): boolean => { + if (fileName === "lib.d.ts") { + return false; + } + fileExistsCalled = true; + assert.isTrue(fileName.indexOf("/f1.") !== -1); + return originalFileExists.call(host, fileName); + }; + + const newContent = `import {x} from "f1"`; + root.content = newContent; + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); + assert.isTrue(fileExistsCalled); + } + }); + + it("loads missing files from disk", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;` + }; + + const files = [root, libFile]; + const host = createWatchedSystem(files); + const originalFileExists = host.fileExists; + + let fileExistsCalledForBar = false; + host.fileExists = fileName => { + if (fileName === "lib.d.ts") { + return false; + } + if (!fileExistsCalledForBar) { + fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + } + + return originalFileExists.call(host, fileName); + }; + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") + ]); + + fileExistsCalledForBar = false; + root.content = `import {y} from "bar"`; + host.reloadFS(files.concat(imported)); + + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }); + + it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;export const x = 10;` + }; + + const files = [root, libFile]; + const filesWithImported = files.concat(imported); + const host = createWatchedSystem(filesWithImported); + const originalFileExists = host.fileExists; + let fileExistsCalledForBar = false; + host.fileExists = fileName => { + if (fileName === "lib.d.ts") { + return false; + } + if (!fileExistsCalledForBar) { + fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + } + return originalFileExists.call(host, fileName); + }; + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputErrorsInitial(host, emptyArray); + + fileExistsCalledForBar = false; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") + ]); + + fileExistsCalledForBar = false; + host.reloadFS(filesWithImported); + host.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(host, emptyArray); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }); + + it("works when module resolution changes to ambient module", () => { + const root = { + path: "/a/b/foo.ts", + content: `import * as fs from "fs";` + }; + + const packageJson = { + path: "/a/b/node_modules/@types/node/package.json", + content: ` +{ + "main": "" +} +` + }; + + const nodeType = { + path: "/a/b/node_modules/@types/node/index.d.ts", + content: ` +declare module "fs" { + export interface Stats { + isFile(): boolean; + } +}` + }; + + const files = [root, libFile]; + const filesWithNodeType = files.concat(packageJson, nodeType); + const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { }); + + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") + ]); + + host.reloadFS(filesWithNodeType); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("works when included file with ambient module changes", () => { + const root = { + path: "/a/b/foo.ts", + content: ` +import * as fs from "fs"; +import * as u from "url"; +` + }; + + const file = { + path: "/a/b/bar.d.ts", + content: ` +declare module "url" { + export interface Url { + href?: string; + } +} +` + }; + + const fileContentWithFS = ` +declare module "fs" { + export interface Stats { + isFile(): boolean; + } +} +`; + + const files = [root, file, libFile]; + const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); + + const watch = createWatchOfFilesAndCompilerOptions([root.path, file.path], host, {}); + + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") + ]); + + file.content += fileContentWithFS; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("works when reusing program with files from external library", () => { + interface ExpectedFile { path: string; isExpectedToEmit?: boolean; content?: string; } + const configDir = "/a/b/projects/myProject/src/"; + const file1: File = { + path: configDir + "file1.ts", + content: 'import module1 = require("module1");\nmodule1("hello");' + }; + const file2: File = { + path: configDir + "file2.ts", + content: 'import module11 = require("module1");\nmodule11("hello");' + }; + const module1: File = { + path: "/a/b/projects/myProject/node_modules/module1/index.js", + content: "module.exports = options => { return options.toString(); }" + }; + const configFile: File = { + path: configDir + "tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + allowJs: true, + rootDir: ".", + outDir: "../dist", + moduleResolution: "node", + maxNodeModuleJsDepth: 1 + } + }) + }; + const outDirFolder = "/a/b/projects/myProject/dist/"; + const programFiles = [file1, file2, module1, libFile]; + const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" }); + const watch = createWatchOfConfigFile(configFile.path, host); + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrorsInitial(host, emptyArray); + const expectedFiles: ExpectedFile[] = [ + createExpectedEmittedFile(file1), + createExpectedEmittedFile(file2), + createExpectedToNotEmitFile("index.js"), + createExpectedToNotEmitFile("src/index.js"), + createExpectedToNotEmitFile("src/file1.js"), + createExpectedToNotEmitFile("src/file2.js"), + createExpectedToNotEmitFile("lib.js"), + createExpectedToNotEmitFile("lib.d.ts") + ]; + verifyExpectedFiles(expectedFiles); + + file1.content += "\n;"; + expectedFiles[0].content += ";\n"; // Only emit file1 with this change + expectedFiles[1].isExpectedToEmit = false; + host.reloadFS(programFiles.concat(configFile)); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrorsIncremental(host, emptyArray); + verifyExpectedFiles(expectedFiles); + + + function verifyExpectedFiles(expectedFiles: ExpectedFile[]) { + forEach(expectedFiles, f => { + assert.equal(!!host.fileExists(f.path), f.isExpectedToEmit, "File " + f.path + " is expected to " + (f.isExpectedToEmit ? "emit" : "not emit")); + if (f.isExpectedToEmit) { + assert.equal(host.readFile(f.path), f.content, "Expected contents of " + f.path); + } + }); + } + + function createExpectedToNotEmitFile(fileName: string): ExpectedFile { + return { + path: outDirFolder + fileName, + isExpectedToEmit: false + }; + } + + function createExpectedEmittedFile(file: File): ExpectedFile { + return { + path: removeFileExtension(file.path.replace(configDir, outDirFolder)) + Extension.Js, + isExpectedToEmit: true, + content: '"use strict";\nexports.__esModule = true;\n' + file.content.replace("import", "var") + "\n" + }; + } + }); + + it("works when renaming node_modules folder that already contains @types folder", () => { + const currentDirectory = "/user/username/projects/myproject"; + const file: File = { + path: `${currentDirectory}/a.ts`, + content: `import * as q from "qqq";` + }; + const module: File = { + path: `${currentDirectory}/node_modules2/@types/qqq/index.d.ts`, + content: "export {}" + }; + const files = [file, module, libFile]; + const host = createWatchedSystem(files, { currentDirectory }); + const watch = createWatchOfFilesAndCompilerOptions([file.path], host); + + checkProgramActualFiles(watch(), [file.path, libFile.path]); + checkOutputErrorsInitial(host, [getDiagnosticModuleNotFoundOfFile(watch(), file, "qqq")]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, [`${currentDirectory}/node_modules`, `${currentDirectory}/node_modules/@types`], /*recursive*/ true); + + host.renameFolder(`${currentDirectory}/node_modules2`, `${currentDirectory}/node_modules`); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), [file.path, libFile.path, `${currentDirectory}/node_modules/@types/qqq/index.d.ts`]); + checkOutputErrorsIncremental(host, emptyArray); + }); + + describe("ignores files/folder changes in node_modules that start with '.'", () => { + const projectPath = "/user/username/projects/project"; + const npmCacheFile: File = { + path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, + content: JSON.stringify({ something: 10 }) + }; + const file1: File = { + path: `${projectPath}/test.ts`, + content: `import { x } from "somemodule";` + }; + const file2: File = { + path: `${projectPath}/node_modules/somemodule/index.d.ts`, + content: `export const x = 10;` + }; + const files = [libFile, file1, file2]; + const expectedFiles = files.map(f => f.path); + it("when watching node_modules in inferred project for failed lookup", () => { + const host = createWatchedSystem(files); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host, {}, /*maxNumberOfFilesToIterateForInvalidation*/ 1); + checkProgramActualFiles(watch(), expectedFiles); + host.checkTimeoutQueueLength(0); + + host.ensureFileOrFolder(npmCacheFile); + host.checkTimeoutQueueLength(0); + }); + it("when watching node_modules as part of wild card directories in config project", () => { + const config: File = { + path: `${projectPath}/tsconfig.json`, + content: "{}" + }; + const host = createWatchedSystem(files.concat(config)); + const watch = createWatchOfConfigFile(config.path, host); + checkProgramActualFiles(watch(), expectedFiles); + host.checkTimeoutQueueLength(0); + + host.ensureFileOrFolder(npmCacheFile); + host.checkTimeoutQueueLength(0); + }); + }); + }); + + describe("resolutionCache:: tsc-watch with modules linked to sibling folder", () => { + const projectRoot = "/user/username/projects/project"; + const mainPackageRoot = `${projectRoot}/main`; + const linkedPackageRoot = `${projectRoot}/linked-package`; + const mainFile: File = { + path: `${mainPackageRoot}/index.ts`, + content: "import { Foo } from '@scoped/linked-package'" + }; + const config: File = { + path: `${mainPackageRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, + files: ["index.ts"] + }) + }; + const linkedPackageInMain: SymLink = { + path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, + symLink: `${linkedPackageRoot}` + }; + const linkedPackageJson: File = { + path: `${linkedPackageRoot}/package.json`, + content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) + }; + const linkedPackageIndex: File = { + path: `${linkedPackageRoot}/dist/index.d.ts`, + content: "export * from './other';" + }; + const linkedPackageOther: File = { + path: `${linkedPackageRoot}/dist/other.d.ts`, + content: 'export declare const Foo = "BAR";' + }; + + it("verify watched directories", () => { + const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; + const host = createWatchedSystem(files, { currentDirectory: mainPackageRoot }); + createWatchOfConfigFile("tsconfig.json", host); + checkWatchedFilesDetailed(host, [libFile.path, mainFile.path, config.path, linkedPackageIndex.path, linkedPackageOther.path], 1); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, [`${mainPackageRoot}/@scoped`, `${mainPackageRoot}/node_modules`, linkedPackageRoot, `${mainPackageRoot}/node_modules/@types`, `${projectRoot}/node_modules/@types`], 1, /*recursive*/ true); + }); + }); +} diff --git a/src/testRunner/unittests/watchApi.ts b/src/testRunner/unittests/tscWatch/watchApi.ts similarity index 100% rename from src/testRunner/unittests/watchApi.ts rename to src/testRunner/unittests/tscWatch/watchApi.ts diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts new file mode 100644 index 00000000000..646fb59ef09 --- /dev/null +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -0,0 +1,178 @@ +namespace ts.tscWatch { + import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; + describe("watchEnvironment:: tsc-watch with different polling/non polling options", () => { + it("watchFile using dynamic priority polling", () => { + const projectFolder = "/a/username/project"; + const file1: File = { + path: `${projectFolder}/typescript.ts`, + content: "var z = 10;" + }; + const files = [file1, libFile]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling"); + const host = createWatchedSystem(files, { environmentVariables }); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); + + const initialProgram = watch(); + verifyProgram(); + + const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium]; + for (let index = 0; index < mediumPollingIntervalThreshold; index++) { + // Transition libFile and file1 to low priority queue + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), initialProgram); + } + + // Make a change to file + file1.content = "var zz30 = 100;"; + host.reloadFS(files); + + // This should detect change in the file + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), initialProgram); + + // Callbacks: medium priority + high priority queue and scheduled program update + host.checkTimeoutQueueLengthAndRun(3); + // During this timeout the file would be detected as unchanged + let fileUnchangeDetected = 1; + const newProgram = watch(); + assert.notStrictEqual(newProgram, initialProgram); + + verifyProgram(); + const outputFile1 = changeExtension(file1.path, ".js"); + assert.isTrue(host.fileExists(outputFile1)); + assert.equal(host.readFile(outputFile1), file1.content + host.newLine); + + const newThreshold = unchangedPollThresholds[PollingInterval.Low] + mediumPollingIntervalThreshold; + for (; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) { + // For high + Medium/low polling interval + host.checkTimeoutQueueLengthAndRun(2); + assert.deepEqual(watch(), newProgram); + } + + // Everything goes in high polling interval queue + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), newProgram); + + function verifyProgram() { + checkProgramActualFiles(watch(), files.map(f => f.path)); + checkWatchedFiles(host, []); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, [], /*recursive*/ true); + } + }); + + describe("tsc-watch when watchDirectories implementation", () => { + function verifyRenamingFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: File = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const file: File = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + const programFiles = [file, libFile]; + const files = [file, configFile, libFile]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); + const host = createWatchedSystem(files, { environmentVariables }); + const watch = createWatchOfConfigFile(configFile.path, host); + const projectFolders = [projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`]; + // Watching files config file, file, lib file + const expectedWatchedFiles = files.map(f => f.path); + const expectedWatchedDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? projectFolders : emptyArray; + if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) { + expectedWatchedFiles.push(...projectFolders); + } + + verifyProgram(checkOutputErrorsInitial); + + // Rename the file: + file.path = file.path.replace("file1.ts", "file2.ts"); + expectedWatchedFiles[0] = file.path; + host.reloadFS(files); + if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { + // With dynamic polling the fs change would be detected only by running timeouts + host.runQueuedTimeoutCallbacks(); + } + // Delayed update program + host.runQueuedTimeoutCallbacks(); + verifyProgram(checkOutputErrorsIncremental); + + function verifyProgram(checkOutputErrors: (host: WatchedSystem, errors: ReadonlyArray) => void) { + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrors(host, emptyArray); + + const outputFile = changeExtension(file.path, ".js"); + assert(host.fileExists(outputFile)); + assert.equal(host.readFile(outputFile), file.content); + + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + + // Watching config file, file, lib file and directories + checkWatchedFilesDetailed(host, expectedWatchedFiles, 1); + checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, 1, /*recursive*/ false); + } + } + + it("uses watchFile when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(Tsc_WatchDirectory.WatchFile); + }); + + it("uses non recursive watchDirectory when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory); + }); + + it("uses non recursive dynamic polling when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); + }); + + it("when there are symlinks to folders in recursive folders", () => { + const cwd = "/home/user/projects/myproject"; + const file1: File = { + path: `${cwd}/src/file.ts`, + content: `import * as a from "a"` + }; + const tsconfig: File = { + path: `${cwd}/tsconfig.json`, + content: `{ "compilerOptions": { "extendedDiagnostics": true, "traceResolution": true }}` + }; + const realA: File = { + path: `${cwd}/node_modules/reala/index.d.ts`, + content: `export {}` + }; + const realB: File = { + path: `${cwd}/node_modules/realb/index.d.ts`, + content: `export {}` + }; + const symLinkA: SymLink = { + path: `${cwd}/node_modules/a`, + symLink: `${cwd}/node_modules/reala` + }; + const symLinkB: SymLink = { + path: `${cwd}/node_modules/b`, + symLink: `${cwd}/node_modules/realb` + }; + const symLinkBInA: SymLink = { + path: `${cwd}/node_modules/reala/node_modules/b`, + symLink: `${cwd}/node_modules/b` + }; + const symLinkAInB: SymLink = { + path: `${cwd}/node_modules/realb/node_modules/a`, + symLink: `${cwd}/node_modules/a` + }; + const files = [file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory); + const host = createWatchedSystem(files, { environmentVariables, currentDirectory: cwd }); + createWatchOfConfigFile("tsconfig.json", host); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + checkWatchedDirectories(host, [cwd, `${cwd}/node_modules`, `${cwd}/node_modules/@types`, `${cwd}/node_modules/reala`, `${cwd}/node_modules/realb`, + `${cwd}/node_modules/reala/node_modules`, `${cwd}/node_modules/realb/node_modules`, `${cwd}/src`], /*recursive*/ false); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserverCachingFileSystemInformation.ts b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts similarity index 98% rename from src/testRunner/unittests/tsserverCachingFileSystemInformation.ts rename to src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts index 65e1ec564d3..b5132f57c0e 100644 --- a/src/testRunner/unittests/tsserverCachingFileSystemInformation.ts +++ b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts @@ -3,7 +3,7 @@ namespace ts.projectSystem { return countWhere(recursiveWatchedDirs, dir => file.length > dir.length && startsWith(file, dir) && file[dir.length] === directorySeparator); } - describe("tsserverCachingFileSystemInformation:: tsserverProjectSystem CachingFileSystemInformation", () => { + describe("tsserver:: CachingFileSystemInformation:: tsserverProjectSystem CachingFileSystemInformation", () => { enum CalledMapsWithSingleArg { fileExists = "fileExists", directoryExists = "directoryExists", diff --git a/src/testRunner/unittests/compileOnSave.ts b/src/testRunner/unittests/tsserver/compileOnSave.ts similarity index 97% rename from src/testRunner/unittests/compileOnSave.ts rename to src/testRunner/unittests/tsserver/compileOnSave.ts index 2352ac58c19..310a6e9e33c 100644 --- a/src/testRunner/unittests/compileOnSave.ts +++ b/src/testRunner/unittests/tsserver/compileOnSave.ts @@ -6,7 +6,7 @@ namespace ts.projectSystem { return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host); } - describe("compileOnSave:: affected list", () => { + describe("tsserver:: compileOnSave:: affected list", () => { function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: File[] }[]) { const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[]; const actualResult = response.sort((list1, list2) => compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); @@ -552,7 +552,7 @@ namespace ts.projectSystem { }); }); - describe("compileOnSave:: EmitFile test", () => { + describe("tsserver:: compileOnSave:: EmitFile test", () => { it("should respect line endings", () => { test("\n"); test("\r\n"); diff --git a/src/testRunner/unittests/tsserverLargeFileReferencedEvent.ts b/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts similarity index 95% rename from src/testRunner/unittests/tsserverLargeFileReferencedEvent.ts rename to src/testRunner/unittests/tsserver/events/largeFileReferenced.ts index f33f32dfe58..ca7f839e1b0 100644 --- a/src/testRunner/unittests/tsserverLargeFileReferencedEvent.ts +++ b/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserverLargeFileReferencedEvent:: LargeFileReferencedEvent with large file", () => { + describe("tsserver:: LargeFileReferencedEvent with large file", () => { const projectRoot = "/user/username/projects/project"; function getLargeFile(useLargeTsFile: boolean) { diff --git a/src/testRunner/unittests/tsserverProjectLoadingEvents.ts b/src/testRunner/unittests/tsserver/events/projectLoading.ts similarity index 96% rename from src/testRunner/unittests/tsserverProjectLoadingEvents.ts rename to src/testRunner/unittests/tsserver/events/projectLoading.ts index 0ff256bd692..2de3ab40db6 100644 --- a/src/testRunner/unittests/tsserverProjectLoadingEvents.ts +++ b/src/testRunner/unittests/tsserver/events/projectLoading.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserverProjectLoadingEvents:: ProjectLoadingStart and ProjectLoadingFinish events", () => { + describe("tsserver:: events:: ProjectLoadingStart and ProjectLoadingFinish events", () => { const projectRoot = "/user/username/projects"; const aTs: File = { path: `${projectRoot}/a/a.ts`, diff --git a/src/testRunner/unittests/tsserverProjectUpdatedInBackgroundEvent.ts b/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts similarity index 97% rename from src/testRunner/unittests/tsserverProjectUpdatedInBackgroundEvent.ts rename to src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts index 3e8382fff58..c818f4d2a8d 100644 --- a/src/testRunner/unittests/tsserverProjectUpdatedInBackgroundEvent.ts +++ b/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserverProjectUpdatedInBackgroundEvent:: ProjectsUpdatedInBackground", () => { + describe("tsserver:: ProjectsUpdatedInBackground", () => { function verifyFiles(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`); const seen = createMap(); diff --git a/src/testRunner/unittests/tsserver/externalProjects.ts b/src/testRunner/unittests/tsserver/externalProjects.ts new file mode 100644 index 00000000000..6c246a2e3bd --- /dev/null +++ b/src/testRunner/unittests/tsserver/externalProjects.ts @@ -0,0 +1,300 @@ +namespace ts.projectSystem { + describe("tsserver:: ExternalProjects", () => { + describe("correctly handling add/remove tsconfig - 1", () => { + function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1;" + }; + const f2 = { + path: "/a/b/lib.ts", + content: "" + }; + const tsconfig = { + path: "/a/b/tsconfig.json", + content: "" + }; + const host = createServerHost([f1, f2]); + const projectService = createProjectService(host); + projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + + // open external project + const projectName = "/a/b/proj1"; + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); + + // rename lib.ts to tsconfig.json + host.reloadFS([f1, tsconfig]); + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, tsconfig.path]), + options: {} + }); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + if (lazyConfiguredProjectsFromExternalProject) { + checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed + projectService.ensureInferredProjectsUpToDate_TestOnly(); + } + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]); + + // rename tsconfig.json back to lib.ts + host.reloadFS([f1, f2]); + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); + } + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); + }); + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); + }); + }); + + describe("correctly handling add/remove tsconfig - 2", () => { + function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1;" + }; + const cLib = { + path: "/a/b/c/lib.ts", + content: "" + }; + const cTsconfig = { + path: "/a/b/c/tsconfig.json", + content: "{}" + }; + const dLib = { + path: "/a/b/d/lib.ts", + content: "" + }; + const dTsconfig = { + path: "/a/b/d/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, cLib, cTsconfig, dLib, dTsconfig]); + const projectService = createProjectService(host); + projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + + // open external project + const projectName = "/a/b/proj1"; + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path]), + options: {} + }); + + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); + + // add two config file as root files + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), + options: {} + }); + projectService.checkNumberOfProjects({ configuredProjects: 2 }); + if (lazyConfiguredProjectsFromExternalProject) { + checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed + checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed + projectService.ensureInferredProjectsUpToDate_TestOnly(); + } + checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); + + // remove one config file + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, dTsconfig.path]), + options: {} + }); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [dLib.path, dTsconfig.path]); + + // remove second config file + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path]), + options: {} + }); + + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); + + // open two config files + // add two config file as root files + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), + options: {} + }); + projectService.checkNumberOfProjects({ configuredProjects: 2 }); + if (lazyConfiguredProjectsFromExternalProject) { + checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed + checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed + projectService.ensureInferredProjectsUpToDate_TestOnly(); + } + checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); + + // close all projects - no projects should be opened + projectService.closeExternalProject(projectName); + projectService.checkNumberOfProjects({}); + } + + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); + }); + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); + }); + }); + + it("correctly handles changes in lib section of config file", () => { + const libES5 = { + path: "/compiler/lib.es5.d.ts", + content: "declare const eval: any" + }; + const libES2015Promise = { + path: "/compiler/lib.es2015.promise.d.ts", + content: "declare class Promise {}" + }; + const app = { + path: "/src/app.ts", + content: "var x: Promise;" + }; + const config1 = { + path: "/src/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: true, + sourceMap: false, + lib: [ + "es5" + ] + } + }) + }; + const config2 = { + path: config1.path, + content: JSON.stringify( + { + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: true, + sourceMap: false, + lib: [ + "es5", + "es2015.promise" + ] + } + }) + }; + const host = createServerHost([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); + const projectService = createProjectService(host); + projectService.openClientFile(app.path); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, app.path, config1.path]); + + host.reloadFS([libES5, libES2015Promise, app, config2]); + host.checkTimeoutQueueLengthAndRun(2); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, libES2015Promise.path, app.path, config2.path]); + }); + + it("should handle non-existing directories in config file", () => { + const f = { + path: "/a/src/app.ts", + content: "let x = 1;" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + include: [ + "src/**/*", + "notexistingfolder/*" + ] + }) + }; + const host = createServerHost([f, config]); + const projectService = createProjectService(host); + projectService.openClientFile(f.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const project = projectService.configuredProjects.get(config.path)!; + assert.isTrue(project.hasOpenRef()); // f + + projectService.closeClientFile(f.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config.path), project); + 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.isTrue(project.hasOpenRef()); // f + assert.isFalse(project.isClosed()); + }); + + it("handles loads existing configured projects of external projects when lazyConfiguredProjectsFromExternalProject is disabled", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({}) + }; + const projectFileName = "/a/b/project.csproj"; + const host = createServerHost([f1, config]); + const service = createProjectService(host); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: true } }); + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path, config.path]), + options: {} + }); + service.checkNumberOfProjects({ configuredProjects: 1 }); + const project = service.configuredProjects.get(config.path)!; + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded + checkProjectActualFiles(project, emptyArray); + + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded + checkProjectActualFiles(project, [config.path, f1.path]); + + service.closeExternalProject(projectFileName); + service.checkNumberOfProjects({}); + + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path, config.path]), + options: {} + }); + service.checkNumberOfProjects({ configuredProjects: 1 }); + const project2 = service.configuredProjects.get(config.path)!; + assert.equal(project2.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded + checkProjectActualFiles(project2, [config.path, f1.path]); + }); + }); +} diff --git a/src/testRunner/unittests/tsserverHelpers.ts b/src/testRunner/unittests/tsserver/helpers.ts similarity index 100% rename from src/testRunner/unittests/tsserverHelpers.ts rename to src/testRunner/unittests/tsserver/helpers.ts diff --git a/src/testRunner/unittests/projectErrors.ts b/src/testRunner/unittests/tsserver/projectErrors.ts similarity index 100% rename from src/testRunner/unittests/projectErrors.ts rename to src/testRunner/unittests/tsserver/projectErrors.ts diff --git a/src/testRunner/unittests/tsserver/reload.ts b/src/testRunner/unittests/tsserver/reload.ts new file mode 100644 index 00000000000..80f3e56e437 --- /dev/null +++ b/src/testRunner/unittests/tsserver/reload.ts @@ -0,0 +1,151 @@ +namespace ts.projectSystem { + describe("tsserver:: reload", () => { + it("should work with temp file", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const tmp = { + path: "/a/b/app.tmp", + content: "const y = 42" + }; + const host = createServerHost([f1, tmp]); + const session = createSession(host); + + // send open request + session.executeCommand({ + type: "request", + command: "open", + seq: 1, + arguments: { file: f1.path } + }); + + // reload from tmp file + session.executeCommand({ + type: "request", + command: "reload", + seq: 2, + arguments: { file: f1.path, tmpfile: tmp.path } + }); + + // verify content + const projectServiice = session.getProjectService(); + const snap1 = projectServiice.getScriptInfo(f1.path)!.getSnapshot(); + assert.equal(getSnapshotText(snap1), tmp.content, "content should be equal to the content of temp file"); + + // reload from original file file + session.executeCommand({ + type: "request", + command: "reload", + seq: 2, + arguments: { file: f1.path } + }); + + // verify content + const snap2 = projectServiice.getScriptInfo(f1.path)!.getSnapshot(); + assert.equal(getSnapshotText(snap2), f1.content, "content should be equal to the content of original file"); + + }); + + it("should work when script info doesnt have any project open", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const tmp = { + path: "/a/b/app.tmp", + content: "const y = 42" + }; + const host = createServerHost([f1, tmp, libFile]); + const session = createSession(host); + const openContent = "let z = 1"; + // send open request + session.executeCommandSeq({ + command: server.protocol.CommandTypes.Open, + arguments: { file: f1.path, fileContent: openContent } + }); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const info = projectService.getScriptInfo(f1.path)!; + assert.isDefined(info); + checkScriptInfoContents(openContent, "contents set during open request"); + + // send close request + session.executeCommandSeq({ + command: server.protocol.CommandTypes.Close, + arguments: { file: f1.path } + }); + checkScriptInfoAndProjects(f1.content, "contents of closed file"); + checkInferredProjectIsOrphan(); + + // Can reload contents of the file when its not open and has no project + // reload from temp file + session.executeCommandSeq({ + command: server.protocol.CommandTypes.Reload, + arguments: { file: f1.path, tmpfile: tmp.path } + }); + checkScriptInfoAndProjects(tmp.content, "contents of temp file"); + checkInferredProjectIsOrphan(); + + // reload from own file + session.executeCommandSeq({ + command: server.protocol.CommandTypes.Reload, + arguments: { file: f1.path } + }); + checkScriptInfoAndProjects(f1.content, "contents of closed file"); + checkInferredProjectIsOrphan(); + + // Open file again without setting its content + session.executeCommandSeq({ + command: server.protocol.CommandTypes.Open, + arguments: { file: f1.path } + }); + checkScriptInfoAndProjects(f1.content, "contents of file when opened without specifying contents"); + const snap = info.getSnapshot(); + + // send close request + session.executeCommandSeq({ + command: server.protocol.CommandTypes.Close, + arguments: { file: f1.path } + }); + checkScriptInfoAndProjects(f1.content, "contents of closed file"); + assert.strictEqual(info.getSnapshot(), snap); + checkInferredProjectIsOrphan(); + + // reload from temp file + session.executeCommandSeq({ + command: server.protocol.CommandTypes.Reload, + arguments: { file: f1.path, tmpfile: tmp.path } + }); + checkScriptInfoAndProjects(tmp.content, "contents of temp file"); + assert.notStrictEqual(info.getSnapshot(), snap); + checkInferredProjectIsOrphan(); + + // reload from own file + session.executeCommandSeq({ + command: server.protocol.CommandTypes.Reload, + arguments: { file: f1.path } + }); + checkScriptInfoAndProjects(f1.content, "contents of closed file"); + assert.notStrictEqual(info.getSnapshot(), snap); + checkInferredProjectIsOrphan(); + + function checkInferredProjectIsOrphan() { + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + assert.equal(info.containingProjects.length, 0); + } + + function checkScriptInfoAndProjects(contentsOfInfo: string, captionForContents: string) { + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + assert.strictEqual(projectService.getScriptInfo(f1.path), info); + checkScriptInfoContents(contentsOfInfo, captionForContents); + } + + function checkScriptInfoContents(contentsOfInfo: string, captionForContents: string) { + const snap = info.getSnapshot(); + assert.equal(getSnapshotText(snap), contentsOfInfo, "content should be equal to " + captionForContents); + } + }); + }); +} diff --git a/src/testRunner/unittests/resolutionCache.ts b/src/testRunner/unittests/tsserver/resolutionCache.ts similarity index 71% rename from src/testRunner/unittests/resolutionCache.ts rename to src/testRunner/unittests/tsserver/resolutionCache.ts index d97770e21b5..a07cb7721d4 100644 --- a/src/testRunner/unittests/resolutionCache.ts +++ b/src/testRunner/unittests/tsserver/resolutionCache.ts @@ -1,452 +1,3 @@ -namespace ts.tscWatch { - describe("resolutionCache:: tsc-watch module resolution caching", () => { - it("works", () => { - const root = { - path: "/a/d/f0.ts", - content: `import {x} from "f1"` - }; - const imported = { - path: "/a/f1.ts", - content: `foo()` - }; - - const files = [root, imported, libFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); - const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); - - // ensure that imported file was found - checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); - - const originalFileExists = host.fileExists; - { - const newContent = `import {x} from "f1" - var x: string = 1;`; - root.content = newContent; - host.reloadFS(files); - - // patch fileExists to make sure that disk is not touched - host.fileExists = notImplemented; - - // trigger synchronization to make sure that import will be fetched from the cache - host.runQueuedTimeoutCallbacks(); - - // ensure file has correct number of errors after edit - checkOutputErrorsIncremental(host, [ - f1IsNotModule, - getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), - cannotFindFoo - ]); - } - { - let fileExistsIsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsIsCalled = true; - assert.isTrue(fileName.indexOf("/f2.") !== -1); - return originalFileExists.call(host, fileName); - }; - - root.content = `import {x} from "f2"`; - host.reloadFS(files); - - // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk - host.runQueuedTimeoutCallbacks(); - - // ensure file has correct number of errors after edit - checkOutputErrorsIncremental(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") - ]); - - assert.isTrue(fileExistsIsCalled); - } - { - let fileExistsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsCalled = true; - assert.isTrue(fileName.indexOf("/f1.") !== -1); - return originalFileExists.call(host, fileName); - }; - - const newContent = `import {x} from "f1"`; - root.content = newContent; - - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - - checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); - assert.isTrue(fileExistsCalled); - } - }); - - it("loads missing files from disk", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; - - const imported = { - path: `/a/bar.d.ts`, - content: `export const y = 1;` - }; - - const files = [root, libFile]; - const host = createWatchedSystem(files); - const originalFileExists = host.fileExists; - - let fileExistsCalledForBar = false; - host.fileExists = fileName => { - if (fileName === "lib.d.ts") { - return false; - } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } - - return originalFileExists.call(host, fileName); - }; - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ]); - - fileExistsCalledForBar = false; - root.content = `import {y} from "bar"`; - host.reloadFS(files.concat(imported)); - - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - }); - - it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; - - const imported = { - path: `/a/bar.d.ts`, - content: `export const y = 1;export const x = 10;` - }; - - const files = [root, libFile]; - const filesWithImported = files.concat(imported); - const host = createWatchedSystem(filesWithImported); - const originalFileExists = host.fileExists; - let fileExistsCalledForBar = false; - host.fileExists = fileName => { - if (fileName === "lib.d.ts") { - return false; - } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } - return originalFileExists.call(host, fileName); - }; - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrorsInitial(host, emptyArray); - - fileExistsCalledForBar = false; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - checkOutputErrorsIncremental(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ]); - - fileExistsCalledForBar = false; - host.reloadFS(filesWithImported); - host.checkTimeoutQueueLengthAndRun(1); - checkOutputErrorsIncremental(host, emptyArray); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - }); - - it("works when module resolution changes to ambient module", () => { - const root = { - path: "/a/b/foo.ts", - content: `import * as fs from "fs";` - }; - - const packageJson = { - path: "/a/b/node_modules/@types/node/package.json", - content: ` -{ - "main": "" -} -` - }; - - const nodeType = { - path: "/a/b/node_modules/@types/node/index.d.ts", - content: ` -declare module "fs" { - export interface Stats { - isFile(): boolean; - } -}` - }; - - const files = [root, libFile]; - const filesWithNodeType = files.concat(packageJson, nodeType); - const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { }); - - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ]); - - host.reloadFS(filesWithNodeType); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("works when included file with ambient module changes", () => { - const root = { - path: "/a/b/foo.ts", - content: ` -import * as fs from "fs"; -import * as u from "url"; -` - }; - - const file = { - path: "/a/b/bar.d.ts", - content: ` -declare module "url" { - export interface Url { - href?: string; - } -} -` - }; - - const fileContentWithFS = ` -declare module "fs" { - export interface Stats { - isFile(): boolean; - } -} -`; - - const files = [root, file, libFile]; - const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); - - const watch = createWatchOfFilesAndCompilerOptions([root.path, file.path], host, {}); - - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ]); - - file.content += fileContentWithFS; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("works when reusing program with files from external library", () => { - interface ExpectedFile { path: string; isExpectedToEmit?: boolean; content?: string; } - const configDir = "/a/b/projects/myProject/src/"; - const file1: File = { - path: configDir + "file1.ts", - content: 'import module1 = require("module1");\nmodule1("hello");' - }; - const file2: File = { - path: configDir + "file2.ts", - content: 'import module11 = require("module1");\nmodule11("hello");' - }; - const module1: File = { - path: "/a/b/projects/myProject/node_modules/module1/index.js", - content: "module.exports = options => { return options.toString(); }" - }; - const configFile: File = { - path: configDir + "tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - allowJs: true, - rootDir: ".", - outDir: "../dist", - moduleResolution: "node", - maxNodeModuleJsDepth: 1 - } - }) - }; - const outDirFolder = "/a/b/projects/myProject/dist/"; - const programFiles = [file1, file2, module1, libFile]; - const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" }); - const watch = createWatchOfConfigFile(configFile.path, host); - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrorsInitial(host, emptyArray); - const expectedFiles: ExpectedFile[] = [ - createExpectedEmittedFile(file1), - createExpectedEmittedFile(file2), - createExpectedToNotEmitFile("index.js"), - createExpectedToNotEmitFile("src/index.js"), - createExpectedToNotEmitFile("src/file1.js"), - createExpectedToNotEmitFile("src/file2.js"), - createExpectedToNotEmitFile("lib.js"), - createExpectedToNotEmitFile("lib.d.ts") - ]; - verifyExpectedFiles(expectedFiles); - - file1.content += "\n;"; - expectedFiles[0].content += ";\n"; // Only emit file1 with this change - expectedFiles[1].isExpectedToEmit = false; - host.reloadFS(programFiles.concat(configFile)); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrorsIncremental(host, emptyArray); - verifyExpectedFiles(expectedFiles); - - - function verifyExpectedFiles(expectedFiles: ExpectedFile[]) { - forEach(expectedFiles, f => { - assert.equal(!!host.fileExists(f.path), f.isExpectedToEmit, "File " + f.path + " is expected to " + (f.isExpectedToEmit ? "emit" : "not emit")); - if (f.isExpectedToEmit) { - assert.equal(host.readFile(f.path), f.content, "Expected contents of " + f.path); - } - }); - } - - function createExpectedToNotEmitFile(fileName: string): ExpectedFile { - return { - path: outDirFolder + fileName, - isExpectedToEmit: false - }; - } - - function createExpectedEmittedFile(file: File): ExpectedFile { - return { - path: removeFileExtension(file.path.replace(configDir, outDirFolder)) + Extension.Js, - isExpectedToEmit: true, - content: '"use strict";\nexports.__esModule = true;\n' + file.content.replace("import", "var") + "\n" - }; - } - }); - - it("works when renaming node_modules folder that already contains @types folder", () => { - const currentDirectory = "/user/username/projects/myproject"; - const file: File = { - path: `${currentDirectory}/a.ts`, - content: `import * as q from "qqq";` - }; - const module: File = { - path: `${currentDirectory}/node_modules2/@types/qqq/index.d.ts`, - content: "export {}" - }; - const files = [file, module, libFile]; - const host = createWatchedSystem(files, { currentDirectory }); - const watch = createWatchOfFilesAndCompilerOptions([file.path], host); - - checkProgramActualFiles(watch(), [file.path, libFile.path]); - checkOutputErrorsInitial(host, [getDiagnosticModuleNotFoundOfFile(watch(), file, "qqq")]); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectories(host, [`${currentDirectory}/node_modules`, `${currentDirectory}/node_modules/@types`], /*recursive*/ true); - - host.renameFolder(`${currentDirectory}/node_modules2`, `${currentDirectory}/node_modules`); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), [file.path, libFile.path, `${currentDirectory}/node_modules/@types/qqq/index.d.ts`]); - checkOutputErrorsIncremental(host, emptyArray); - }); - - describe("ignores files/folder changes in node_modules that start with '.'", () => { - const projectPath = "/user/username/projects/project"; - const npmCacheFile: File = { - path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, - content: JSON.stringify({ something: 10 }) - }; - const file1: File = { - path: `${projectPath}/test.ts`, - content: `import { x } from "somemodule";` - }; - const file2: File = { - path: `${projectPath}/node_modules/somemodule/index.d.ts`, - content: `export const x = 10;` - }; - const files = [libFile, file1, file2]; - const expectedFiles = files.map(f => f.path); - it("when watching node_modules in inferred project for failed lookup", () => { - const host = createWatchedSystem(files); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host, {}, /*maxNumberOfFilesToIterateForInvalidation*/ 1); - checkProgramActualFiles(watch(), expectedFiles); - host.checkTimeoutQueueLength(0); - - host.ensureFileOrFolder(npmCacheFile); - host.checkTimeoutQueueLength(0); - }); - it("when watching node_modules as part of wild card directories in config project", () => { - const config: File = { - path: `${projectPath}/tsconfig.json`, - content: "{}" - }; - const host = createWatchedSystem(files.concat(config)); - const watch = createWatchOfConfigFile(config.path, host); - checkProgramActualFiles(watch(), expectedFiles); - host.checkTimeoutQueueLength(0); - - host.ensureFileOrFolder(npmCacheFile); - host.checkTimeoutQueueLength(0); - }); - }); - }); - - describe("resolutionCache:: tsc-watch with modules linked to sibling folder", () => { - const projectRoot = "/user/username/projects/project"; - const mainPackageRoot = `${projectRoot}/main`; - const linkedPackageRoot = `${projectRoot}/linked-package`; - const mainFile: File = { - path: `${mainPackageRoot}/index.ts`, - content: "import { Foo } from '@scoped/linked-package'" - }; - const config: File = { - path: `${mainPackageRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, - files: ["index.ts"] - }) - }; - const linkedPackageInMain: SymLink = { - path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, - symLink: `${linkedPackageRoot}` - }; - const linkedPackageJson: File = { - path: `${linkedPackageRoot}/package.json`, - content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) - }; - const linkedPackageIndex: File = { - path: `${linkedPackageRoot}/dist/index.d.ts`, - content: "export * from './other';" - }; - const linkedPackageOther: File = { - path: `${linkedPackageRoot}/dist/other.d.ts`, - content: 'export declare const Foo = "BAR";' - }; - - it("verify watched directories", () => { - const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; - const host = createWatchedSystem(files, { currentDirectory: mainPackageRoot }); - createWatchOfConfigFile("tsconfig.json", host); - checkWatchedFilesDetailed(host, [libFile.path, mainFile.path, config.path, linkedPackageIndex.path, linkedPackageOther.path], 1); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectoriesDetailed(host, [`${mainPackageRoot}/@scoped`, `${mainPackageRoot}/node_modules`, linkedPackageRoot, `${mainPackageRoot}/node_modules/@types`, `${projectRoot}/node_modules/@types`], 1, /*recursive*/ true); - }); - }); -} - namespace ts.projectSystem { function createHostModuleResolutionTrace(host: TestServerHost & ModuleResolutionHost) { const resolutionTrace: string[] = []; diff --git a/src/testRunner/unittests/session.ts b/src/testRunner/unittests/tsserver/session.ts similarity index 100% rename from src/testRunner/unittests/session.ts rename to src/testRunner/unittests/tsserver/session.ts diff --git a/src/testRunner/unittests/tsserver/skipLibCheck.ts b/src/testRunner/unittests/tsserver/skipLibCheck.ts new file mode 100644 index 00000000000..463ea439396 --- /dev/null +++ b/src/testRunner/unittests/tsserver/skipLibCheck.ts @@ -0,0 +1,229 @@ +namespace ts.projectSystem { + describe("tsserver:: with skipLibCheck", () => { + it("should be turned on for js-only inferred projects", () => { + const file1 = { + path: "/a/b/file1.js", + content: ` + /// + var x = 1;` + }; + const file2 = { + path: "/a/b/file2.d.ts", + content: ` + interface T { + name: string; + }; + interface T { + name: number; + };` + }; + const host = createServerHost([file1, file2]); + const session = createSession(host); + openFilesForSession([file1, file2], session); + + const file2GetErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: file2.path } + ); + let errorResult = session.executeCommand(file2GetErrRequest).response; + assert.isTrue(errorResult.length === 0); + + const closeFileRequest = makeSessionRequest(CommandNames.Close, { file: file1.path }); + session.executeCommand(closeFileRequest); + errorResult = session.executeCommand(file2GetErrRequest).response; + assert.isTrue(errorResult.length !== 0); + + openFilesForSession([file1], session); + errorResult = session.executeCommand(file2GetErrRequest).response; + assert.isTrue(errorResult.length === 0); + }); + + it("should be turned on for js-only external projects", () => { + const jsFile = { + path: "/a/b/file1.js", + content: "let x =1;" + }; + const dTsFile = { + path: "/a/b/file2.d.ts", + content: ` + interface T { + name: string; + }; + interface T { + name: number; + };` + }; + const host = createServerHost([jsFile, dTsFile]); + const session = createSession(host); + + const openExternalProjectRequest = makeSessionRequest( + CommandNames.OpenExternalProject, + { + projectFileName: "project1", + rootFiles: toExternalFiles([jsFile.path, dTsFile.path]), + options: {} + } + ); + session.executeCommand(openExternalProjectRequest); + + const dTsFileGetErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: dTsFile.path } + ); + const errorResult = session.executeCommand(dTsFileGetErrRequest).response; + assert.isTrue(errorResult.length === 0); + }); + + it("should be turned on for js-only external projects with skipLibCheck=false", () => { + const jsFile = { + path: "/a/b/file1.js", + content: "let x =1;" + }; + const dTsFile = { + path: "/a/b/file2.d.ts", + content: ` + interface T { + name: string; + }; + interface T { + name: number; + };` + }; + const host = createServerHost([jsFile, dTsFile]); + const session = createSession(host); + + const openExternalProjectRequest = makeSessionRequest( + CommandNames.OpenExternalProject, + { + projectFileName: "project1", + rootFiles: toExternalFiles([jsFile.path, dTsFile.path]), + options: { skipLibCheck: false } + } + ); + session.executeCommand(openExternalProjectRequest); + + const dTsFileGetErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: dTsFile.path } + ); + const errorResult = session.executeCommand(dTsFileGetErrRequest).response; + assert.isTrue(errorResult.length === 0); + }); + + it("should not report bind errors for declaration files with skipLibCheck=true", () => { + const jsconfigFile = { + path: "/a/jsconfig.json", + content: "{}" + }; + const jsFile = { + path: "/a/jsFile.js", + content: "let x = 1;" + }; + const dTsFile1 = { + path: "/a/dTsFile1.d.ts", + content: ` + declare var x: number;` + }; + const dTsFile2 = { + path: "/a/dTsFile2.d.ts", + content: ` + declare var x: string;` + }; + const host = createServerHost([jsconfigFile, jsFile, dTsFile1, dTsFile2]); + const session = createSession(host); + openFilesForSession([jsFile], session); + + const dTsFile1GetErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: dTsFile1.path } + ); + const error1Result = session.executeCommand(dTsFile1GetErrRequest).response; + assert.isTrue(error1Result.length === 0); + + const dTsFile2GetErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: dTsFile2.path } + ); + const error2Result = session.executeCommand(dTsFile2GetErrRequest).response; + assert.isTrue(error2Result.length === 0); + }); + + it("should report semantic errors for loose JS files with '// @ts-check' and skipLibCheck=true", () => { + const jsFile = { + path: "/a/jsFile.js", + content: ` + // @ts-check + let x = 1; + x === "string";` + }; + + const host = createServerHost([jsFile]); + const session = createSession(host); + openFilesForSession([jsFile], session); + + const getErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: jsFile.path } + ); + const errorResult = session.executeCommand(getErrRequest).response; + assert.isTrue(errorResult.length === 1); + assert.equal(errorResult[0].code, Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap.code); + }); + + it("should report semantic errors for configured js project with '// @ts-check' and skipLibCheck=true", () => { + const jsconfigFile = { + path: "/a/jsconfig.json", + content: "{}" + }; + + const jsFile = { + path: "/a/jsFile.js", + content: ` + // @ts-check + let x = 1; + x === "string";` + }; + + const host = createServerHost([jsconfigFile, jsFile]); + const session = createSession(host); + openFilesForSession([jsFile], session); + + const getErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: jsFile.path } + ); + const errorResult = session.executeCommand(getErrRequest).response; + assert.isTrue(errorResult.length === 1); + assert.equal(errorResult[0].code, Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap.code); + }); + + it("should report semantic errors for configured js project with checkJs=true and skipLibCheck=true", () => { + const jsconfigFile = { + path: "/a/jsconfig.json", + content: JSON.stringify({ + compilerOptions: { + checkJs: true, + skipLibCheck: true + }, + }) + }; + const jsFile = { + path: "/a/jsFile.js", + content: `let x = 1; + x === "string";` + }; + + const host = createServerHost([jsconfigFile, jsFile]); + const session = createSession(host); + openFilesForSession([jsFile], session); + + const getErrRequest = makeSessionRequest( + CommandNames.SemanticDiagnosticsSync, + { file: jsFile.path } + ); + const errorResult = session.executeCommand(getErrRequest).response; + assert.isTrue(errorResult.length === 1); + assert.equal(errorResult[0].code, Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap.code); + }); + }); +} diff --git a/src/testRunner/unittests/tsserverSymLinks.ts b/src/testRunner/unittests/tsserver/symLinks.ts similarity index 97% rename from src/testRunner/unittests/tsserverSymLinks.ts rename to src/testRunner/unittests/tsserver/symLinks.ts index c9bad7ea678..d6fa9dca894 100644 --- a/src/testRunner/unittests/tsserverSymLinks.ts +++ b/src/testRunner/unittests/tsserver/symLinks.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserverProjectSystem with symLinks", () => { + describe("tsserver:: symLinks", () => { it("rename in common file renames all project", () => { const projects = "/users/username/projects"; const folderA = `${projects}/a`; diff --git a/src/testRunner/unittests/telemetry.ts b/src/testRunner/unittests/tsserver/telemetry.ts similarity index 100% rename from src/testRunner/unittests/telemetry.ts rename to src/testRunner/unittests/tsserver/telemetry.ts diff --git a/src/testRunner/unittests/textStorage.ts b/src/testRunner/unittests/tsserver/textStorage.ts similarity index 100% rename from src/testRunner/unittests/textStorage.ts rename to src/testRunner/unittests/tsserver/textStorage.ts diff --git a/src/testRunner/unittests/typingsInstaller.ts b/src/testRunner/unittests/tsserver/typingsInstaller.ts similarity index 100% rename from src/testRunner/unittests/typingsInstaller.ts rename to src/testRunner/unittests/tsserver/typingsInstaller.ts diff --git a/src/testRunner/unittests/versionCache.ts b/src/testRunner/unittests/tsserver/versionCache.ts similarity index 100% rename from src/testRunner/unittests/versionCache.ts rename to src/testRunner/unittests/tsserver/versionCache.ts diff --git a/src/testRunner/unittests/tsserver/watchEnvironment.ts b/src/testRunner/unittests/tsserver/watchEnvironment.ts new file mode 100644 index 00000000000..8bce3505626 --- /dev/null +++ b/src/testRunner/unittests/tsserver/watchEnvironment.ts @@ -0,0 +1,135 @@ +namespace ts.projectSystem { + import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; + describe("watchEnvironment:: tsserverProjectSystem watchDirectories implementation", () => { + function verifyCompletionListWithNewFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: File = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const index: File = { + path: `${projectSrcFolder}/index.ts`, + content: `import {} from "./"` + }; + const file1: File = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + + const files = [index, file1, configFile, libFile]; + const fileNames = files.map(file => file.path); + // All closed files(files other than index), project folder, project/src folder and project/node_modules/@types folder + const expectedWatchedFiles = arrayToMap(fileNames.slice(1), s => s, () => 1); + const expectedWatchedDirectories = createMap(); + const mapOfDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? + expectedWatchedDirectories : + tscWatchDirectory === Tsc_WatchDirectory.WatchFile ? + expectedWatchedFiles : + createMap(); + // For failed resolution lookup and tsconfig files => cached so only watched only once + mapOfDirectories.set(projectFolder, 1); + // Through above recursive watches + mapOfDirectories.set(projectSrcFolder, 1); + // node_modules/@types folder + mapOfDirectories.set(`${projectFolder}/${nodeModulesAtTypes}`, 1); + const expectedCompletions = ["file1"]; + const completionPosition = index.content.lastIndexOf('"'); + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); + const host = createServerHost(files, { environmentVariables }); + const projectService = createProjectService(host); + projectService.openClientFile(index.path); + + const project = Debug.assertDefined(projectService.configuredProjects.get(configFile.path)); + verifyProjectAndCompletions(); + + // Add file2 + const file2: File = { + path: `${projectSrcFolder}/file2.ts`, + content: "" + }; + files.push(file2); + fileNames.push(file2.path); + expectedWatchedFiles.set(file2.path, 1); + expectedCompletions.push("file2"); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + assert.equal(projectService.configuredProjects.get(configFile.path), project); + verifyProjectAndCompletions(); + + function verifyProjectAndCompletions() { + const completions = project.getLanguageService().getCompletionsAtPosition(index.path, completionPosition, { includeExternalModuleExports: false, includeInsertTextCompletions: false })!; + checkArray("Completion Entries", completions.entries.map(e => e.name), expectedCompletions); + + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + + checkWatchedFilesDetailed(host, expectedWatchedFiles); + checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, /*recursive*/ false); + checkProjectActualFiles(project, fileNames); + } + } + + it("uses watchFile when file is added to subfolder, completion list has new file", () => { + verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.WatchFile); + }); + + it("uses non recursive watchDirectory when file is added to subfolder, completion list has new file", () => { + verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory); + }); + + it("uses dynamic polling when file is added to subfolder, completion list has new file", () => { + verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); + }); + }); + + describe("watchEnvironment:: tsserverProjectSystem Watched recursive directories with windows style file system", () => { + function verifyWatchedDirectories(rootedPath: string, useProjectAtRoot: boolean) { + const root = useProjectAtRoot ? rootedPath : `${rootedPath}myfolder/allproject/`; + const configFile: File = { + path: root + "project/tsconfig.json", + content: "{}" + }; + const file1: File = { + path: root + "project/file1.ts", + content: "let x = 10;" + }; + const file2: File = { + 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); + } + + function verifyRootedDirectoryWatch(rootedPath: string) { + it("When project is in rootFolder of style c:/", () => { + verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ true); + }); + + it("When files at some folder other than root", () => { + verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ false); + }); + } + + describe("for rootFolder of style c:/", () => { + verifyRootedDirectoryWatch("c:/"); + }); + + describe("for rootFolder of style c:/users/username", () => { + verifyRootedDirectoryWatch("c:/users/username/"); + }); + }); +} diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 56493c01e35..85be445171a 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -3280,305 +3280,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem external projects", () => { - describe("correctly handling add/remove tsconfig - 1", () => { - function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1;" - }; - const f2 = { - path: "/a/b/lib.ts", - content: "" - }; - const tsconfig = { - path: "/a/b/tsconfig.json", - content: "" - }; - const host = createServerHost([f1, f2]); - const projectService = createProjectService(host); - projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - - // open external project - const projectName = "/a/b/proj1"; - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path, f2.path]), - options: {} - }); - projectService.openClientFile(f1.path); - projectService.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); - - // rename lib.ts to tsconfig.json - host.reloadFS([f1, tsconfig]); - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path, tsconfig.path]), - options: {} - }); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - if (lazyConfiguredProjectsFromExternalProject) { - checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed - projectService.ensureInferredProjectsUpToDate_TestOnly(); - } - checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]); - - // rename tsconfig.json back to lib.ts - host.reloadFS([f1, f2]); - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path, f2.path]), - options: {} - }); - - projectService.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); - } - it("when lazyConfiguredProjectsFromExternalProject not set", () => { - verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); - }); - it("when lazyConfiguredProjectsFromExternalProject is set", () => { - verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); - }); - }); - - describe("correctly handling add/remove tsconfig - 2", () => { - function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1;" - }; - const cLib = { - path: "/a/b/c/lib.ts", - content: "" - }; - const cTsconfig = { - path: "/a/b/c/tsconfig.json", - content: "{}" - }; - const dLib = { - path: "/a/b/d/lib.ts", - content: "" - }; - const dTsconfig = { - path: "/a/b/d/tsconfig.json", - content: "{}" - }; - const host = createServerHost([f1, cLib, cTsconfig, dLib, dTsconfig]); - const projectService = createProjectService(host); - projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - - // open external project - const projectName = "/a/b/proj1"; - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path]), - options: {} - }); - - projectService.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); - - // add two config file as root files - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), - options: {} - }); - projectService.checkNumberOfProjects({ configuredProjects: 2 }); - if (lazyConfiguredProjectsFromExternalProject) { - checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed - checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed - projectService.ensureInferredProjectsUpToDate_TestOnly(); - } - checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); - checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); - - // remove one config file - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path, dTsconfig.path]), - options: {} - }); - - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [dLib.path, dTsconfig.path]); - - // remove second config file - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path]), - options: {} - }); - - projectService.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); - - // open two config files - // add two config file as root files - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), - options: {} - }); - projectService.checkNumberOfProjects({ configuredProjects: 2 }); - if (lazyConfiguredProjectsFromExternalProject) { - checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed - checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed - projectService.ensureInferredProjectsUpToDate_TestOnly(); - } - checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); - checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); - - // close all projects - no projects should be opened - projectService.closeExternalProject(projectName); - projectService.checkNumberOfProjects({}); - } - - it("when lazyConfiguredProjectsFromExternalProject not set", () => { - verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); - }); - it("when lazyConfiguredProjectsFromExternalProject is set", () => { - verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); - }); - }); - - it("correctly handles changes in lib section of config file", () => { - const libES5 = { - path: "/compiler/lib.es5.d.ts", - content: "declare const eval: any" - }; - const libES2015Promise = { - path: "/compiler/lib.es2015.promise.d.ts", - content: "declare class Promise {}" - }; - const app = { - path: "/src/app.ts", - content: "var x: Promise;" - }; - const config1 = { - path: "/src/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: true, - sourceMap: false, - lib: [ - "es5" - ] - } - }) - }; - const config2 = { - path: config1.path, - content: JSON.stringify( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: true, - sourceMap: false, - lib: [ - "es5", - "es2015.promise" - ] - } - }) - }; - const host = createServerHost([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); - const projectService = createProjectService(host); - projectService.openClientFile(app.path); - - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, app.path, config1.path]); - - host.reloadFS([libES5, libES2015Promise, app, config2]); - host.checkTimeoutQueueLengthAndRun(2); - - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, libES2015Promise.path, app.path, config2.path]); - }); - - it("should handle non-existing directories in config file", () => { - const f = { - path: "/a/src/app.ts", - content: "let x = 1;" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: {}, - include: [ - "src/**/*", - "notexistingfolder/*" - ] - }) - }; - const host = createServerHost([f, config]); - const projectService = createProjectService(host); - projectService.openClientFile(f.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const project = projectService.configuredProjects.get(config.path)!; - assert.isTrue(project.hasOpenRef()); // f - - projectService.closeClientFile(f.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config.path), project); - 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.isTrue(project.hasOpenRef()); // f - assert.isFalse(project.isClosed()); - }); - - it("handles loads existing configured projects of external projects when lazyConfiguredProjectsFromExternalProject is disabled", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({}) - }; - const projectFileName = "/a/b/project.csproj"; - const host = createServerHost([f1, config]); - const service = createProjectService(host); - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: true } }); - service.openExternalProject({ - projectFileName, - rootFiles: toExternalFiles([f1.path, config.path]), - options: {} - }); - service.checkNumberOfProjects({ configuredProjects: 1 }); - const project = service.configuredProjects.get(config.path)!; - assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded - checkProjectActualFiles(project, emptyArray); - - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); - assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded - checkProjectActualFiles(project, [config.path, f1.path]); - - service.closeExternalProject(projectFileName); - service.checkNumberOfProjects({}); - - service.openExternalProject({ - projectFileName, - rootFiles: toExternalFiles([f1.path, config.path]), - options: {} - }); - service.checkNumberOfProjects({ configuredProjects: 1 }); - const project2 = service.configuredProjects.get(config.path)!; - assert.equal(project2.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded - checkProjectActualFiles(project2, [config.path, f1.path]); - }); - }); - describe("tsserverProjectSystem prefer typings to js", () => { it("during second resolution pass", () => { const typingsCacheLocation = "/a/typings"; @@ -3770,234 +3471,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem skipLibCheck", () => { - it("should be turned on for js-only inferred projects", () => { - const file1 = { - path: "/a/b/file1.js", - content: ` - /// - var x = 1;` - }; - const file2 = { - path: "/a/b/file2.d.ts", - content: ` - interface T { - name: string; - }; - interface T { - name: number; - };` - }; - const host = createServerHost([file1, file2]); - const session = createSession(host); - openFilesForSession([file1, file2], session); - - const file2GetErrRequest = makeSessionRequest( - CommandNames.SemanticDiagnosticsSync, - { file: file2.path } - ); - let errorResult = session.executeCommand(file2GetErrRequest).response; - assert.isTrue(errorResult.length === 0); - - const closeFileRequest = makeSessionRequest(CommandNames.Close, { file: file1.path }); - session.executeCommand(closeFileRequest); - errorResult = session.executeCommand(file2GetErrRequest).response; - assert.isTrue(errorResult.length !== 0); - - openFilesForSession([file1], session); - errorResult = session.executeCommand(file2GetErrRequest).response; - assert.isTrue(errorResult.length === 0); - }); - - it("should be turned on for js-only external projects", () => { - const jsFile = { - path: "/a/b/file1.js", - content: "let x =1;" - }; - const dTsFile = { - path: "/a/b/file2.d.ts", - content: ` - interface T { - name: string; - }; - interface T { - name: number; - };` - }; - const host = createServerHost([jsFile, dTsFile]); - const session = createSession(host); - - const openExternalProjectRequest = makeSessionRequest( - CommandNames.OpenExternalProject, - { - projectFileName: "project1", - rootFiles: toExternalFiles([jsFile.path, dTsFile.path]), - options: {} - } - ); - session.executeCommand(openExternalProjectRequest); - - const dTsFileGetErrRequest = makeSessionRequest( - CommandNames.SemanticDiagnosticsSync, - { file: dTsFile.path } - ); - const errorResult = session.executeCommand(dTsFileGetErrRequest).response; - assert.isTrue(errorResult.length === 0); - }); - - it("should be turned on for js-only external projects with skipLibCheck=false", () => { - const jsFile = { - path: "/a/b/file1.js", - content: "let x =1;" - }; - const dTsFile = { - path: "/a/b/file2.d.ts", - content: ` - interface T { - name: string; - }; - interface T { - name: number; - };` - }; - const host = createServerHost([jsFile, dTsFile]); - const session = createSession(host); - - const openExternalProjectRequest = makeSessionRequest( - CommandNames.OpenExternalProject, - { - projectFileName: "project1", - rootFiles: toExternalFiles([jsFile.path, dTsFile.path]), - options: { skipLibCheck: false } - } - ); - session.executeCommand(openExternalProjectRequest); - - const dTsFileGetErrRequest = makeSessionRequest( - CommandNames.SemanticDiagnosticsSync, - { file: dTsFile.path } - ); - const errorResult = session.executeCommand(dTsFileGetErrRequest).response; - assert.isTrue(errorResult.length === 0); - }); - - it("should not report bind errors for declaration files with skipLibCheck=true", () => { - const jsconfigFile = { - path: "/a/jsconfig.json", - content: "{}" - }; - const jsFile = { - path: "/a/jsFile.js", - content: "let x = 1;" - }; - const dTsFile1 = { - path: "/a/dTsFile1.d.ts", - content: ` - declare var x: number;` - }; - const dTsFile2 = { - path: "/a/dTsFile2.d.ts", - content: ` - declare var x: string;` - }; - const host = createServerHost([jsconfigFile, jsFile, dTsFile1, dTsFile2]); - const session = createSession(host); - openFilesForSession([jsFile], session); - - const dTsFile1GetErrRequest = makeSessionRequest( - CommandNames.SemanticDiagnosticsSync, - { file: dTsFile1.path } - ); - const error1Result = session.executeCommand(dTsFile1GetErrRequest).response; - assert.isTrue(error1Result.length === 0); - - const dTsFile2GetErrRequest = makeSessionRequest( - CommandNames.SemanticDiagnosticsSync, - { file: dTsFile2.path } - ); - const error2Result = session.executeCommand(dTsFile2GetErrRequest).response; - assert.isTrue(error2Result.length === 0); - }); - - it("should report semantic errors for loose JS files with '// @ts-check' and skipLibCheck=true", () => { - const jsFile = { - path: "/a/jsFile.js", - content: ` - // @ts-check - let x = 1; - x === "string";` - }; - - const host = createServerHost([jsFile]); - const session = createSession(host); - openFilesForSession([jsFile], session); - - const getErrRequest = makeSessionRequest( - CommandNames.SemanticDiagnosticsSync, - { file: jsFile.path } - ); - const errorResult = session.executeCommand(getErrRequest).response; - assert.isTrue(errorResult.length === 1); - assert.equal(errorResult[0].code, Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap.code); - }); - - it("should report semantic errors for configured js project with '// @ts-check' and skipLibCheck=true", () => { - const jsconfigFile = { - path: "/a/jsconfig.json", - content: "{}" - }; - - const jsFile = { - path: "/a/jsFile.js", - content: ` - // @ts-check - let x = 1; - x === "string";` - }; - - const host = createServerHost([jsconfigFile, jsFile]); - const session = createSession(host); - openFilesForSession([jsFile], session); - - const getErrRequest = makeSessionRequest( - CommandNames.SemanticDiagnosticsSync, - { file: jsFile.path } - ); - const errorResult = session.executeCommand(getErrRequest).response; - assert.isTrue(errorResult.length === 1); - assert.equal(errorResult[0].code, Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap.code); - }); - - it("should report semantic errors for configured js project with checkJs=true and skipLibCheck=true", () => { - const jsconfigFile = { - path: "/a/jsconfig.json", - content: JSON.stringify({ - compilerOptions: { - checkJs: true, - skipLibCheck: true - }, - }) - }; - const jsFile = { - path: "/a/jsFile.js", - content: `let x = 1; - x === "string";` - }; - - const host = createServerHost([jsconfigFile, jsFile]); - const session = createSession(host); - openFilesForSession([jsFile], session); - - const getErrRequest = makeSessionRequest( - CommandNames.SemanticDiagnosticsSync, - { file: jsFile.path } - ); - const errorResult = session.executeCommand(getErrRequest).response; - assert.isTrue(errorResult.length === 1); - assert.equal(errorResult[0].code, Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap.code); - }); - }); - describe("tsserverProjectSystem non-existing directories listed in config file input array", () => { it("should be tolerated without crashing the server", () => { const configFile = { @@ -4079,156 +3552,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem reload", () => { - it("should work with temp file", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const tmp = { - path: "/a/b/app.tmp", - content: "const y = 42" - }; - const host = createServerHost([f1, tmp]); - const session = createSession(host); - - // send open request - session.executeCommand({ - type: "request", - command: "open", - seq: 1, - arguments: { file: f1.path } - }); - - // reload from tmp file - session.executeCommand({ - type: "request", - command: "reload", - seq: 2, - arguments: { file: f1.path, tmpfile: tmp.path } - }); - - // verify content - const projectServiice = session.getProjectService(); - const snap1 = projectServiice.getScriptInfo(f1.path)!.getSnapshot(); - assert.equal(getSnapshotText(snap1), tmp.content, "content should be equal to the content of temp file"); - - // reload from original file file - session.executeCommand({ - type: "request", - command: "reload", - seq: 2, - arguments: { file: f1.path } - }); - - // verify content - const snap2 = projectServiice.getScriptInfo(f1.path)!.getSnapshot(); - assert.equal(getSnapshotText(snap2), f1.content, "content should be equal to the content of original file"); - - }); - - it("should work when script info doesnt have any project open", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const tmp = { - path: "/a/b/app.tmp", - content: "const y = 42" - }; - const host = createServerHost([f1, tmp, libFile]); - const session = createSession(host); - const openContent = "let z = 1"; - // send open request - session.executeCommandSeq({ - command: server.protocol.CommandTypes.Open, - arguments: { file: f1.path, fileContent: openContent } - }); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const info = projectService.getScriptInfo(f1.path)!; - assert.isDefined(info); - checkScriptInfoContents(openContent, "contents set during open request"); - - // send close request - session.executeCommandSeq({ - command: server.protocol.CommandTypes.Close, - arguments: { file: f1.path } - }); - checkScriptInfoAndProjects(f1.content, "contents of closed file"); - checkInferredProjectIsOrphan(); - - // Can reload contents of the file when its not open and has no project - // reload from temp file - session.executeCommandSeq({ - command: server.protocol.CommandTypes.Reload, - arguments: { file: f1.path, tmpfile: tmp.path } - }); - checkScriptInfoAndProjects(tmp.content, "contents of temp file"); - checkInferredProjectIsOrphan(); - - // reload from own file - session.executeCommandSeq({ - command: server.protocol.CommandTypes.Reload, - arguments: { file: f1.path } - }); - checkScriptInfoAndProjects(f1.content, "contents of closed file"); - checkInferredProjectIsOrphan(); - - // Open file again without setting its content - session.executeCommandSeq({ - command: server.protocol.CommandTypes.Open, - arguments: { file: f1.path } - }); - checkScriptInfoAndProjects(f1.content, "contents of file when opened without specifying contents"); - const snap = info.getSnapshot(); - - // send close request - session.executeCommandSeq({ - command: server.protocol.CommandTypes.Close, - arguments: { file: f1.path } - }); - checkScriptInfoAndProjects(f1.content, "contents of closed file"); - assert.strictEqual(info.getSnapshot(), snap); - checkInferredProjectIsOrphan(); - - // reload from temp file - session.executeCommandSeq({ - command: server.protocol.CommandTypes.Reload, - arguments: { file: f1.path, tmpfile: tmp.path } - }); - checkScriptInfoAndProjects(tmp.content, "contents of temp file"); - assert.notStrictEqual(info.getSnapshot(), snap); - checkInferredProjectIsOrphan(); - - // reload from own file - session.executeCommandSeq({ - command: server.protocol.CommandTypes.Reload, - arguments: { file: f1.path } - }); - checkScriptInfoAndProjects(f1.content, "contents of closed file"); - assert.notStrictEqual(info.getSnapshot(), snap); - checkInferredProjectIsOrphan(); - - function checkInferredProjectIsOrphan() { - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - assert.equal(info.containingProjects.length, 0); - } - - function checkScriptInfoAndProjects(contentsOfInfo: string, captionForContents: string) { - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - assert.strictEqual(projectService.getScriptInfo(f1.path), info); - checkScriptInfoContents(contentsOfInfo, captionForContents); - } - - function checkScriptInfoContents(contentsOfInfo: string, captionForContents: string) { - const snap = info.getSnapshot(); - assert.equal(getSnapshotText(snap), contentsOfInfo, "content should be equal to " + captionForContents); - } - }); - }); - describe("tsserverProjectSystem Inferred projects", () => { it("should support files without extensions", () => { const f = { diff --git a/src/testRunner/unittests/watchEnvironment.ts b/src/testRunner/unittests/watchEnvironment.ts deleted file mode 100644 index 74a6637d6fc..00000000000 --- a/src/testRunner/unittests/watchEnvironment.ts +++ /dev/null @@ -1,316 +0,0 @@ -namespace ts { - import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; - - export namespace tscWatch { - describe("watchEnvironment:: tsc-watch with different polling/non polling options", () => { - it("watchFile using dynamic priority polling", () => { - const projectFolder = "/a/username/project"; - const file1: File = { - path: `${projectFolder}/typescript.ts`, - content: "var z = 10;" - }; - const files = [file1, libFile]; - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling"); - const host = createWatchedSystem(files, { environmentVariables }); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); - - const initialProgram = watch(); - verifyProgram(); - - const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium]; - for (let index = 0; index < mediumPollingIntervalThreshold; index++) { - // Transition libFile and file1 to low priority queue - host.checkTimeoutQueueLengthAndRun(1); - assert.deepEqual(watch(), initialProgram); - } - - // Make a change to file - file1.content = "var zz30 = 100;"; - host.reloadFS(files); - - // This should detect change in the file - host.checkTimeoutQueueLengthAndRun(1); - assert.deepEqual(watch(), initialProgram); - - // Callbacks: medium priority + high priority queue and scheduled program update - host.checkTimeoutQueueLengthAndRun(3); - // During this timeout the file would be detected as unchanged - let fileUnchangeDetected = 1; - const newProgram = watch(); - assert.notStrictEqual(newProgram, initialProgram); - - verifyProgram(); - const outputFile1 = changeExtension(file1.path, ".js"); - assert.isTrue(host.fileExists(outputFile1)); - assert.equal(host.readFile(outputFile1), file1.content + host.newLine); - - const newThreshold = unchangedPollThresholds[PollingInterval.Low] + mediumPollingIntervalThreshold; - for (; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) { - // For high + Medium/low polling interval - host.checkTimeoutQueueLengthAndRun(2); - assert.deepEqual(watch(), newProgram); - } - - // Everything goes in high polling interval queue - host.checkTimeoutQueueLengthAndRun(1); - assert.deepEqual(watch(), newProgram); - - function verifyProgram() { - checkProgramActualFiles(watch(), files.map(f => f.path)); - checkWatchedFiles(host, []); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, [], /*recursive*/ true); - } - }); - - describe("tsc-watch when watchDirectories implementation", () => { - function verifyRenamingFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { - const projectFolder = "/a/username/project"; - const projectSrcFolder = `${projectFolder}/src`; - const configFile: File = { - path: `${projectFolder}/tsconfig.json`, - content: "{}" - }; - const file: File = { - path: `${projectSrcFolder}/file1.ts`, - content: "" - }; - const programFiles = [file, libFile]; - const files = [file, configFile, libFile]; - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); - const host = createWatchedSystem(files, { environmentVariables }); - const watch = createWatchOfConfigFile(configFile.path, host); - const projectFolders = [projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`]; - // Watching files config file, file, lib file - const expectedWatchedFiles = files.map(f => f.path); - const expectedWatchedDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? projectFolders : emptyArray; - if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) { - expectedWatchedFiles.push(...projectFolders); - } - - verifyProgram(checkOutputErrorsInitial); - - // Rename the file: - file.path = file.path.replace("file1.ts", "file2.ts"); - expectedWatchedFiles[0] = file.path; - host.reloadFS(files); - if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { - // With dynamic polling the fs change would be detected only by running timeouts - host.runQueuedTimeoutCallbacks(); - } - // Delayed update program - host.runQueuedTimeoutCallbacks(); - verifyProgram(checkOutputErrorsIncremental); - - function verifyProgram(checkOutputErrors: (host: WatchedSystem, errors: ReadonlyArray) => void) { - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrors(host, emptyArray); - - const outputFile = changeExtension(file.path, ".js"); - assert(host.fileExists(outputFile)); - assert.equal(host.readFile(outputFile), file.content); - - checkWatchedDirectories(host, emptyArray, /*recursive*/ true); - - // Watching config file, file, lib file and directories - checkWatchedFilesDetailed(host, expectedWatchedFiles, 1); - checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, 1, /*recursive*/ false); - } - } - - it("uses watchFile when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(Tsc_WatchDirectory.WatchFile); - }); - - it("uses non recursive watchDirectory when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory); - }); - - it("uses non recursive dynamic polling when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); - }); - - it("when there are symlinks to folders in recursive folders", () => { - const cwd = "/home/user/projects/myproject"; - const file1: File = { - path: `${cwd}/src/file.ts`, - content: `import * as a from "a"` - }; - const tsconfig: File = { - path: `${cwd}/tsconfig.json`, - content: `{ "compilerOptions": { "extendedDiagnostics": true, "traceResolution": true }}` - }; - const realA: File = { - path: `${cwd}/node_modules/reala/index.d.ts`, - content: `export {}` - }; - const realB: File = { - path: `${cwd}/node_modules/realb/index.d.ts`, - content: `export {}` - }; - const symLinkA: SymLink = { - path: `${cwd}/node_modules/a`, - symLink: `${cwd}/node_modules/reala` - }; - const symLinkB: SymLink = { - path: `${cwd}/node_modules/b`, - symLink: `${cwd}/node_modules/realb` - }; - const symLinkBInA: SymLink = { - path: `${cwd}/node_modules/reala/node_modules/b`, - symLink: `${cwd}/node_modules/b` - }; - const symLinkAInB: SymLink = { - path: `${cwd}/node_modules/realb/node_modules/a`, - symLink: `${cwd}/node_modules/a` - }; - const files = [file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB]; - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory); - const host = createWatchedSystem(files, { environmentVariables, currentDirectory: cwd }); - createWatchOfConfigFile("tsconfig.json", host); - checkWatchedDirectories(host, emptyArray, /*recursive*/ true); - checkWatchedDirectories(host, [cwd, `${cwd}/node_modules`, `${cwd}/node_modules/@types`, `${cwd}/node_modules/reala`, `${cwd}/node_modules/realb`, - `${cwd}/node_modules/reala/node_modules`, `${cwd}/node_modules/realb/node_modules`, `${cwd}/src`], /*recursive*/ false); - }); - }); - }); - } - - export namespace projectSystem { - describe("watchEnvironment:: tsserverProjectSystem watchDirectories implementation", () => { - function verifyCompletionListWithNewFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { - const projectFolder = "/a/username/project"; - const projectSrcFolder = `${projectFolder}/src`; - const configFile: File = { - path: `${projectFolder}/tsconfig.json`, - content: "{}" - }; - const index: File = { - path: `${projectSrcFolder}/index.ts`, - content: `import {} from "./"` - }; - const file1: File = { - path: `${projectSrcFolder}/file1.ts`, - content: "" - }; - - const files = [index, file1, configFile, libFile]; - const fileNames = files.map(file => file.path); - // All closed files(files other than index), project folder, project/src folder and project/node_modules/@types folder - const expectedWatchedFiles = arrayToMap(fileNames.slice(1), s => s, () => 1); - const expectedWatchedDirectories = createMap(); - const mapOfDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? - expectedWatchedDirectories : - tscWatchDirectory === Tsc_WatchDirectory.WatchFile ? - expectedWatchedFiles : - createMap(); - // For failed resolution lookup and tsconfig files => cached so only watched only once - mapOfDirectories.set(projectFolder, 1); - // Through above recursive watches - mapOfDirectories.set(projectSrcFolder, 1); - // node_modules/@types folder - mapOfDirectories.set(`${projectFolder}/${nodeModulesAtTypes}`, 1); - const expectedCompletions = ["file1"]; - const completionPosition = index.content.lastIndexOf('"'); - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); - const host = createServerHost(files, { environmentVariables }); - const projectService = createProjectService(host); - projectService.openClientFile(index.path); - - const project = Debug.assertDefined(projectService.configuredProjects.get(configFile.path)); - verifyProjectAndCompletions(); - - // Add file2 - const file2: File = { - path: `${projectSrcFolder}/file2.ts`, - content: "" - }; - files.push(file2); - fileNames.push(file2.path); - expectedWatchedFiles.set(file2.path, 1); - expectedCompletions.push("file2"); - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - assert.equal(projectService.configuredProjects.get(configFile.path), project); - verifyProjectAndCompletions(); - - function verifyProjectAndCompletions() { - const completions = project.getLanguageService().getCompletionsAtPosition(index.path, completionPosition, { includeExternalModuleExports: false, includeInsertTextCompletions: false })!; - checkArray("Completion Entries", completions.entries.map(e => e.name), expectedCompletions); - - checkWatchedDirectories(host, emptyArray, /*recursive*/ true); - - checkWatchedFilesDetailed(host, expectedWatchedFiles); - checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, /*recursive*/ false); - checkProjectActualFiles(project, fileNames); - } - } - - it("uses watchFile when file is added to subfolder, completion list has new file", () => { - verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.WatchFile); - }); - - it("uses non recursive watchDirectory when file is added to subfolder, completion list has new file", () => { - verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory); - }); - - it("uses dynamic polling when file is added to subfolder, completion list has new file", () => { - verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); - }); - }); - - describe("watchEnvironment:: tsserverProjectSystem Watched recursive directories with windows style file system", () => { - function verifyWatchedDirectories(rootedPath: string, useProjectAtRoot: boolean) { - const root = useProjectAtRoot ? rootedPath : `${rootedPath}myfolder/allproject/`; - const configFile: File = { - path: root + "project/tsconfig.json", - content: "{}" - }; - const file1: File = { - path: root + "project/file1.ts", - content: "let x = 10;" - }; - const file2: File = { - 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); - } - - function verifyRootedDirectoryWatch(rootedPath: string) { - it("When project is in rootFolder of style c:/", () => { - verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ true); - }); - - it("When files at some folder other than root", () => { - verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ false); - }); - } - - describe("for rootFolder of style c:/", () => { - verifyRootedDirectoryWatch("c:/"); - }); - - describe("for rootFolder of style c:/users/username", () => { - verifyRootedDirectoryWatch("c:/users/username/"); - }); - }); - } -} From 53e2507f603836e6ff268d1a5e2e87ba2265e4c4 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 16:04:49 -0800 Subject: [PATCH 17/20] More scenarios in their own test --- src/testRunner/tsconfig.json | 26 +- .../unittests/config/commandLineParsing.ts | 4 +- .../config/configurationExtension.ts | 2 +- .../config/convertCompilerOptionsFromJson.ts | 2 +- .../config/convertTypeAcquisitionFromJson.ts | 2 +- .../unittests/config/initializeTSConfig.ts | 4 +- src/testRunner/unittests/config/matchFiles.ts | 2 +- .../unittests/config/projectReferences.ts | 12 +- src/testRunner/unittests/config/showConfig.ts | 2 +- .../unittests/config/tsconfigParsing.ts | 2 +- .../unittests/evaluation/asyncArrow.ts | 4 +- .../unittests/evaluation/asyncGenerator.ts | 4 +- .../unittests/evaluation/forAwaitOf.ts | 2 +- .../cancellableLanguageServiceOperations.ts | 2 +- .../unittests/services/colorization.ts | 2 +- .../services/convertToAsyncFunction.ts | 2 +- .../unittests/services/documentRegistry.ts | 2 +- .../unittests/services/extract/constants.ts | 2 +- .../unittests/services/extract/functions.ts | 2 +- .../unittests/services/extract/ranges.ts | 4 +- .../services/extract/symbolWalker.ts | 2 +- .../unittests/services/hostNewLineSupport.ts | 4 +- .../unittests/services/languageService.ts | 4 +- .../unittests/services/organizeImports.ts | 2 +- .../unittests/services/patternMatcher.ts | 2 +- .../unittests/services/preProcessFile.ts | 2 +- .../unittests/services/textChanges.ts | 4 +- .../unittests/{ => services}/transpile.ts | 2 +- src/testRunner/unittests/tscWatch/emit.ts | 8 +- src/testRunner/unittests/tscWatch/helpers.ts | 9 + .../unittests/tscWatch/resolutionCache.ts | 4 +- src/testRunner/unittests/tscWatch/watchApi.ts | 2 +- .../unittests/tscWatch/watchEnvironment.ts | 2 +- src/testRunner/unittests/tscWatchMode.ts | 9 - .../unittests/tsserver/cancellationToken.ts | 271 ++ .../unittests/tsserver/completions.ts | 122 + .../unittests/tsserver/configFileSearch.ts | 174 + .../unittests/tsserver/declarationFileMaps.ts | 566 +++ .../unittests/tsserver/documentRegistry.ts | 94 + .../unittests/tsserver/duplicatePackages.ts | 54 + .../forceConsistentCasingInFileNames.ts | 45 + .../unittests/tsserver/formatSettings.ts | 39 + .../tsserver/getEditsForFileRename.ts | 105 + src/testRunner/unittests/tsserver/helpers.ts | 58 +- .../unittests/tsserver/importHelpers.ts | 18 + .../unittests/tsserver/inferredProjects.ts | 229 ++ .../unittests/tsserver/languageService.ts | 19 + .../tsserver/maxNodeModuleJsDepth.ts | 55 + .../unittests/tsserver/metadataInResponse.ts | 99 + src/testRunner/unittests/tsserver/navTo.ts | 30 + .../unittests/tsserver/occurences.ts | 47 + src/testRunner/unittests/tsserver/openFile.ts | 108 + .../unittests/tsserver/projectReferences.ts | 658 ++++ .../unittests/tsserver/refactors.ts | 120 + src/testRunner/unittests/tsserver/rename.ts | 53 + .../unittests/tsserver/resolutionCache.ts | 10 +- .../unittests/tsserver/syntaxOperations.ts | 98 + .../unittests/tsserver/telemetry.ts | 2 +- .../unittests/tsserver/textStorage.ts | 2 +- .../unittests/tsserver/typeAquisition.ts | 52 + .../tsserver/typeReferenceDirectives.ts | 87 + .../unittests/tsserver/typingsInstaller.ts | 20 +- .../unittests/tsserver/untitledFiles.ts | 45 + .../unittests/tsserver/versionCache.ts | 6 +- .../unittests/tsserver/watchEnvironment.ts | 4 +- .../unittests/tsserverProjectSystem.ts | 3185 +---------------- 66 files changed, 3308 insertions(+), 3307 deletions(-) rename src/testRunner/unittests/{ => services}/transpile.ts (97%) create mode 100644 src/testRunner/unittests/tsserver/cancellationToken.ts create mode 100644 src/testRunner/unittests/tsserver/completions.ts create mode 100644 src/testRunner/unittests/tsserver/configFileSearch.ts create mode 100644 src/testRunner/unittests/tsserver/declarationFileMaps.ts create mode 100644 src/testRunner/unittests/tsserver/documentRegistry.ts create mode 100644 src/testRunner/unittests/tsserver/duplicatePackages.ts create mode 100644 src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts create mode 100644 src/testRunner/unittests/tsserver/formatSettings.ts create mode 100644 src/testRunner/unittests/tsserver/getEditsForFileRename.ts create mode 100644 src/testRunner/unittests/tsserver/importHelpers.ts create mode 100644 src/testRunner/unittests/tsserver/inferredProjects.ts create mode 100644 src/testRunner/unittests/tsserver/languageService.ts create mode 100644 src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts create mode 100644 src/testRunner/unittests/tsserver/metadataInResponse.ts create mode 100644 src/testRunner/unittests/tsserver/navTo.ts create mode 100644 src/testRunner/unittests/tsserver/occurences.ts create mode 100644 src/testRunner/unittests/tsserver/openFile.ts create mode 100644 src/testRunner/unittests/tsserver/projectReferences.ts create mode 100644 src/testRunner/unittests/tsserver/refactors.ts create mode 100644 src/testRunner/unittests/tsserver/rename.ts create mode 100644 src/testRunner/unittests/tsserver/syntaxOperations.ts create mode 100644 src/testRunner/unittests/tsserver/typeAquisition.ts create mode 100644 src/testRunner/unittests/tsserver/typeReferenceDirectives.ts create mode 100644 src/testRunner/unittests/tsserver/untitledFiles.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index a7d82b84464..4d200c4b8f9 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -58,7 +58,6 @@ "unittests/reuseProgramStructure.ts", "unittests/semver.ts", "unittests/transform.ts", - "unittests/transpile.ts", "unittests/tsbuild.ts", "unittests/tsbuildWatchMode.ts", "unittests/tscWatchMode.ts", @@ -89,25 +88,50 @@ "unittests/services/patternMatcher.ts", "unittests/services/preProcessFile.ts", "unittests/services/textChanges.ts", + "unittests/services/transpile.ts", "unittests/tscWatch/emit.ts", "unittests/tscWatch/resolutionCache.ts", "unittests/tscWatch/watchEnvironment.ts", "unittests/tscWatch/watchApi.ts", "unittests/tsserver/cachingFileSystemInformation.ts", + "unittests/tsserver/cancellationToken.ts", "unittests/tsserver/compileOnSave.ts", + "unittests/tsserver/completions.ts", + "unittests/tsserver/configFileSearch.ts", + "unittests/tsserver/declarationFileMaps.ts", + "unittests/tsserver/documentRegistry.ts", + "unittests/tsserver/duplicatePackages.ts", "unittests/tsserver/events/largeFileReferenced.ts", "unittests/tsserver/events/projectLoading.ts", "unittests/tsserver/events/projectUpdatedInBackground.ts", "unittests/tsserver/externalProjects.ts", + "unittests/tsserver/forceConsistentCasingInFileNames.ts", + "unittests/tsserver/formatSettings.ts", + "unittests/tsserver/getEditsForFileRename.ts", + "unittests/tsserver/importHelpers.ts", + "unittests/tsserver/inferredProjects.ts", + "unittests/tsserver/languageService.ts", + "unittests/tsserver/maxNodeModuleJsDepth.ts", + "unittests/tsserver/metadataInResponse.ts", + "unittests/tsserver/navTo.ts", + "unittests/tsserver/occurences.ts", + "unittests/tsserver/openFile.ts", "unittests/tsserver/projectErrors.ts", + "unittests/tsserver/projectReferences.ts", + "unittests/tsserver/refactors.ts", "unittests/tsserver/reload.ts", + "unittests/tsserver/rename.ts", "unittests/tsserver/resolutionCache.ts", "unittests/tsserver/session.ts", "unittests/tsserver/skipLibCheck.ts", "unittests/tsserver/symLinks.ts", + "unittests/tsserver/syntaxOperations.ts", "unittests/tsserver/textStorage.ts", "unittests/tsserver/telemetry.ts", + "unittests/tsserver/typeAquisition.ts", + "unittests/tsserver/typeReferenceDirectives.ts", "unittests/tsserver/typingsInstaller.ts", + "unittests/tsserver/untitledFiles.ts", "unittests/tsserver/versionCache.ts", "unittests/tsserver/watchEnvironment.ts" ] diff --git a/src/testRunner/unittests/config/commandLineParsing.ts b/src/testRunner/unittests/config/commandLineParsing.ts index 7e8ba8f84bb..be7f6dca3c5 100644 --- a/src/testRunner/unittests/config/commandLineParsing.ts +++ b/src/testRunner/unittests/config/commandLineParsing.ts @@ -1,5 +1,5 @@ namespace ts { - describe("commandLineParsing:: parseCommandLine", () => { + describe("config:: commandLineParsing:: parseCommandLine", () => { function assertParseResult(commandLine: string[], expectedParsedCommandLine: ParsedCommandLine) { const parsed = parseCommandLine(commandLine); @@ -367,7 +367,7 @@ namespace ts { }); }); - describe("commandLineParsing:: parseBuildOptions", () => { + describe("config:: commandLineParsing:: parseBuildOptions", () => { function assertParseResult(commandLine: string[], expectedParsedBuildCommand: ParsedBuildCommand) { const parsed = parseBuildCommand(commandLine); const parsedBuildOptions = JSON.stringify(parsed.buildOptions); diff --git a/src/testRunner/unittests/config/configurationExtension.ts b/src/testRunner/unittests/config/configurationExtension.ts index 57c899eb5a8..0e7cd0821c4 100644 --- a/src/testRunner/unittests/config/configurationExtension.ts +++ b/src/testRunner/unittests/config/configurationExtension.ts @@ -208,7 +208,7 @@ namespace ts { } } - describe("configurationExtension", () => { + describe("config:: configurationExtension", () => { forEach<[string, string, fakes.ParseConfigHost], void>([ ["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost], ["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost] diff --git a/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts b/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts index 7b8af54f930..4a3a42ce216 100644 --- a/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts +++ b/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts @@ -1,5 +1,5 @@ namespace ts { - describe("convertCompilerOptionsFromJson", () => { + describe("config:: convertCompilerOptionsFromJson", () => { const formatDiagnosticHost: FormatDiagnosticsHost = { getCurrentDirectory: () => "/apath/", getCanonicalFileName: createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ true), diff --git a/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts index b46d37f0428..0998b0b0d7a 100644 --- a/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts +++ b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts @@ -1,6 +1,6 @@ namespace ts { interface ExpectedResult { typeAcquisition: TypeAcquisition; errors: Diagnostic[]; } - describe("convertTypeAcquisitionFromJson", () => { + describe("config:: convertTypeAcquisitionFromJson", () => { function assertTypeAcquisition(json: any, configFileName: string, expectedResult: ExpectedResult) { assertTypeAcquisitionWithJson(json, configFileName, expectedResult); assertTypeAcquisitionWithJsonNode(json, configFileName, expectedResult); diff --git a/src/testRunner/unittests/config/initializeTSConfig.ts b/src/testRunner/unittests/config/initializeTSConfig.ts index 679eecf71b1..09d80c7f5c2 100644 --- a/src/testRunner/unittests/config/initializeTSConfig.ts +++ b/src/testRunner/unittests/config/initializeTSConfig.ts @@ -1,5 +1,5 @@ namespace ts { - describe("initTSConfig", () => { + describe("config:: initTSConfig", () => { function initTSConfigCorrectly(name: string, commandLinesArgs: string[]) { describe(name, () => { const commandLine = parseCommandLine(commandLinesArgs); @@ -30,4 +30,4 @@ namespace ts { initTSConfigCorrectly("Initialized TSConfig with advanced options", ["--init", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"]); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/config/matchFiles.ts b/src/testRunner/unittests/config/matchFiles.ts index 5ece71245bb..9155cb26a1c 100644 --- a/src/testRunner/unittests/config/matchFiles.ts +++ b/src/testRunner/unittests/config/matchFiles.ts @@ -143,7 +143,7 @@ namespace ts { return createFileDiagnostic(file, start, length, diagnosticMessage, arg0); } - describe("matchFiles", () => { + describe("config:: matchFiles", () => { it("with defaults", () => { const json = {}; const expected: ParsedCommandLine = { diff --git a/src/testRunner/unittests/config/projectReferences.ts b/src/testRunner/unittests/config/projectReferences.ts index f67faa4609b..be8e0be6206 100644 --- a/src/testRunner/unittests/config/projectReferences.ts +++ b/src/testRunner/unittests/config/projectReferences.ts @@ -96,7 +96,7 @@ namespace ts { checkResult(prog, host); } - describe("project-references meta check", () => { + describe("config:: project-references meta check", () => { it("default setup was created correctly", () => { const spec: TestSpecification = { "/primary": { @@ -118,7 +118,7 @@ namespace ts { /** * Validate that we enforce the basic settings constraints for referenced projects */ - describe("project-references constraint checking for settings", () => { + describe("config:: project-references constraint checking for settings", () => { it("errors when declaration = false", () => { const spec: TestSpecification = { "/primary": { @@ -248,7 +248,7 @@ namespace ts { /** * Path mapping behavior */ - describe("project-references path mapping", () => { + describe("config:: project-references path mapping", () => { it("redirects to the output .d.ts file", () => { const spec: TestSpecification = { "/alpha": { @@ -268,7 +268,7 @@ namespace ts { }); }); - describe("project-references nice-behavior", () => { + describe("config:: project-references nice-behavior", () => { it("issues a nice error when the input file is missing", () => { const spec: TestSpecification = { "/alpha": { @@ -289,7 +289,7 @@ namespace ts { /** * 'composite' behavior */ - describe("project-references behavior changes under composite: true", () => { + describe("config:: project-references behavior changes under composite: true", () => { it("doesn't infer the rootDir from source paths", () => { const spec: TestSpecification = { "/alpha": { @@ -308,7 +308,7 @@ namespace ts { }); }); - describe("project-references errors when a file in a composite project occurs outside the root", () => { + describe("config:: project-references errors when a file in a composite project occurs outside the root", () => { it("Errors when a file is outside the rootdir", () => { const spec: TestSpecification = { "/alpha": { diff --git a/src/testRunner/unittests/config/showConfig.ts b/src/testRunner/unittests/config/showConfig.ts index 4040e6563a8..f86b37970f5 100644 --- a/src/testRunner/unittests/config/showConfig.ts +++ b/src/testRunner/unittests/config/showConfig.ts @@ -1,5 +1,5 @@ namespace ts { - describe("showConfig", () => { + describe("config:: showConfig", () => { function showTSConfigCorrectly(name: string, commandLinesArgs: string[], configJson?: object) { describe(name, () => { const outputFileName = `showConfig/${name.replace(/[^a-z0-9\-./ ]/ig, "")}/tsconfig.json`; diff --git a/src/testRunner/unittests/config/tsconfigParsing.ts b/src/testRunner/unittests/config/tsconfigParsing.ts index 909af429d6e..cc617a0d151 100644 --- a/src/testRunner/unittests/config/tsconfigParsing.ts +++ b/src/testRunner/unittests/config/tsconfigParsing.ts @@ -1,5 +1,5 @@ namespace ts { - describe("tsconfigParsing:: parseConfigFileTextToJson", () => { + describe("config:: tsconfigParsing:: parseConfigFileTextToJson", () => { function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; error?: Diagnostic[] }) { const parsed = parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject)); diff --git a/src/testRunner/unittests/evaluation/asyncArrow.ts b/src/testRunner/unittests/evaluation/asyncArrow.ts index 994fe8a84be..01959ca150f 100644 --- a/src/testRunner/unittests/evaluation/asyncArrow.ts +++ b/src/testRunner/unittests/evaluation/asyncArrow.ts @@ -1,4 +1,4 @@ -describe("asyncArrowEvaluation", () => { +describe("evaluation:: asyncArrowEvaluation", () => { // https://github.com/Microsoft/TypeScript/issues/24722 it("this capture (es5)", async () => { const result = evaluator.evaluateTypeScript(` @@ -15,4 +15,4 @@ describe("asyncArrowEvaluation", () => { await result.main(); assert.instanceOf(result.output[0].a(), result.A); }); -}); \ No newline at end of file +}); diff --git a/src/testRunner/unittests/evaluation/asyncGenerator.ts b/src/testRunner/unittests/evaluation/asyncGenerator.ts index 9963ea921fa..cb630d28724 100644 --- a/src/testRunner/unittests/evaluation/asyncGenerator.ts +++ b/src/testRunner/unittests/evaluation/asyncGenerator.ts @@ -1,4 +1,4 @@ -describe("asyncGeneratorEvaluation", () => { +describe("evaluation:: asyncGeneratorEvaluation", () => { it("return (es5)", async () => { const result = evaluator.evaluateTypeScript(` async function * g() { @@ -27,4 +27,4 @@ describe("asyncGeneratorEvaluation", () => { { value: 0, done: true } ]); }); -}); \ No newline at end of file +}); diff --git a/src/testRunner/unittests/evaluation/forAwaitOf.ts b/src/testRunner/unittests/evaluation/forAwaitOf.ts index 20ab5eed0cc..7e7ee41e2f1 100644 --- a/src/testRunner/unittests/evaluation/forAwaitOf.ts +++ b/src/testRunner/unittests/evaluation/forAwaitOf.ts @@ -1,4 +1,4 @@ -describe("forAwaitOfEvaluation", () => { +describe("evaluation:: forAwaitOfEvaluation", () => { it("sync (es5)", async () => { const result = evaluator.evaluateTypeScript(` let i = 0; diff --git a/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts b/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts index 37f829d67a7..1d7e12d9825 100644 --- a/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts +++ b/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts @@ -1,5 +1,5 @@ namespace ts { - describe("cancellableLanguageServiceOperations", () => { + describe("services:: cancellableLanguageServiceOperations", () => { const file = ` function foo(): void; function foo(x: T): T; diff --git a/src/testRunner/unittests/services/colorization.ts b/src/testRunner/unittests/services/colorization.ts index e3295e6fee8..54ea98dd563 100644 --- a/src/testRunner/unittests/services/colorization.ts +++ b/src/testRunner/unittests/services/colorization.ts @@ -6,7 +6,7 @@ interface ClassificationEntry { position?: number; } -describe("Colorization", () => { +describe("services:: Colorization", () => { // Use the shim adapter to ensure test coverage of the shim layer for the classifier const languageServiceAdapter = new Harness.LanguageService.ShimLanguageServiceAdapter(/*preprocessToResolve*/ false); const classifier = languageServiceAdapter.getClassifier(); diff --git a/src/testRunner/unittests/services/convertToAsyncFunction.ts b/src/testRunner/unittests/services/convertToAsyncFunction.ts index 21c6a29f8e4..e8d857f2ee0 100644 --- a/src/testRunner/unittests/services/convertToAsyncFunction.ts +++ b/src/testRunner/unittests/services/convertToAsyncFunction.ts @@ -343,7 +343,7 @@ interface Array {}` } } - describe("convertToAsyncFunctions", () => { + describe("services:: convertToAsyncFunctions", () => { _testConvertToAsyncFunction("convertToAsyncFunction_basic", ` function [#|f|](): Promise{ return fetch('https://typescriptlang.org').then(result => { console.log(result) }); diff --git a/src/testRunner/unittests/services/documentRegistry.ts b/src/testRunner/unittests/services/documentRegistry.ts index a3dad56f42b..96f2aaff76b 100644 --- a/src/testRunner/unittests/services/documentRegistry.ts +++ b/src/testRunner/unittests/services/documentRegistry.ts @@ -1,4 +1,4 @@ -describe("DocumentRegistry", () => { +describe("services:: DocumentRegistry", () => { it("documents are shared between projects", () => { const documentRegistry = ts.createDocumentRegistry(); const defaultCompilerOptions = ts.getDefaultCompilerOptions(); diff --git a/src/testRunner/unittests/services/extract/constants.ts b/src/testRunner/unittests/services/extract/constants.ts index e0ef305812c..1ca5a719a82 100644 --- a/src/testRunner/unittests/services/extract/constants.ts +++ b/src/testRunner/unittests/services/extract/constants.ts @@ -1,5 +1,5 @@ namespace ts { - describe("extractConstants", () => { + describe("services:: extract:: extractConstants", () => { testExtractConstant("extractConstant_TopLevel", `let x = [#|1|];`); diff --git a/src/testRunner/unittests/services/extract/functions.ts b/src/testRunner/unittests/services/extract/functions.ts index 1f90e1cc600..21c5e8e9001 100644 --- a/src/testRunner/unittests/services/extract/functions.ts +++ b/src/testRunner/unittests/services/extract/functions.ts @@ -1,5 +1,5 @@ namespace ts { - describe("extractFunctions", () => { + describe("services:: extract:: extractFunctions", () => { testExtractFunction("extractFunction1", `namespace A { let x = 1; diff --git a/src/testRunner/unittests/services/extract/ranges.ts b/src/testRunner/unittests/services/extract/ranges.ts index 9cd76dd49e9..dc62618e0c6 100644 --- a/src/testRunner/unittests/services/extract/ranges.ts +++ b/src/testRunner/unittests/services/extract/ranges.ts @@ -42,7 +42,7 @@ namespace ts { } } - describe("extractRanges", () => { + describe("services:: extract:: extractRanges", () => { it("get extract range from selection", () => { testExtractRange(` [#| @@ -418,4 +418,4 @@ switch (x) { testExtractRangeFailed("extract-method-not-for-token-expression-statement", `[#|a|]`, [refactor.extractSymbol.Messages.cannotExtractIdentifier.message]); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/services/extract/symbolWalker.ts b/src/testRunner/unittests/services/extract/symbolWalker.ts index a027f0f2ce7..9d323a7544a 100644 --- a/src/testRunner/unittests/services/extract/symbolWalker.ts +++ b/src/testRunner/unittests/services/extract/symbolWalker.ts @@ -1,5 +1,5 @@ namespace ts { - describe("Symbol Walker", () => { + describe("services:: extract:: Symbol Walker", () => { function test(description: string, source: string, verifier: (file: SourceFile, checker: TypeChecker) => void) { it(description, () => { const result = Harness.Compiler.compileFiles([{ diff --git a/src/testRunner/unittests/services/hostNewLineSupport.ts b/src/testRunner/unittests/services/hostNewLineSupport.ts index abd79210086..2f00c7b08e5 100644 --- a/src/testRunner/unittests/services/hostNewLineSupport.ts +++ b/src/testRunner/unittests/services/hostNewLineSupport.ts @@ -1,5 +1,5 @@ namespace ts { - describe("hostNewLineSupport", () => { + describe("services:: hostNewLineSupport", () => { function testLSWithFiles(settings: CompilerOptions, files: Harness.Compiler.TestFile[]) { function snapFor(path: string): IScriptSnapshot | undefined { if (path === "lib.d.ts") { @@ -46,4 +46,4 @@ namespace ts { `); }); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/services/languageService.ts b/src/testRunner/unittests/services/languageService.ts index ce8fa93d1f9..332d59ca68a 100644 --- a/src/testRunner/unittests/services/languageService.ts +++ b/src/testRunner/unittests/services/languageService.ts @@ -1,5 +1,5 @@ namespace ts { - describe("languageService", () => { + describe("services:: languageService", () => { const files: {[index: string]: string} = { "foo.ts": `import Vue from "./vue"; import Component from "./vue-class-component"; @@ -43,4 +43,4 @@ export function Component(x: Config): any;` expect(definitions).to.exist; // tslint:disable-line no-unused-expression }); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index 355292c6d89..a45c7548394 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -1,5 +1,5 @@ namespace ts { - describe("Organize imports", () => { + describe("services:: Organize imports", () => { describe("Sort imports", () => { it("Sort - non-relative vs non-relative", () => { assertSortsBefore( diff --git a/src/testRunner/unittests/services/patternMatcher.ts b/src/testRunner/unittests/services/patternMatcher.ts index 5e35d2020db..304a9225e0d 100644 --- a/src/testRunner/unittests/services/patternMatcher.ts +++ b/src/testRunner/unittests/services/patternMatcher.ts @@ -1,4 +1,4 @@ -describe("PatternMatcher", () => { +describe("services:: PatternMatcher", () => { describe("BreakIntoCharacterSpans", () => { it("EmptyIdentifier", () => { verifyBreakIntoCharacterSpans(""); diff --git a/src/testRunner/unittests/services/preProcessFile.ts b/src/testRunner/unittests/services/preProcessFile.ts index a89b6337c84..caf412d6403 100644 --- a/src/testRunner/unittests/services/preProcessFile.ts +++ b/src/testRunner/unittests/services/preProcessFile.ts @@ -1,4 +1,4 @@ -describe("PreProcessFile:", () => { +describe("services:: PreProcessFile:", () => { function test(sourceText: string, readImportFile: boolean, detectJavaScriptImports: boolean, expectedPreProcess: ts.PreProcessedFileInfo): void { const resultPreProcess = ts.preProcessFile(sourceText, readImportFile, detectJavaScriptImports); diff --git a/src/testRunner/unittests/services/textChanges.ts b/src/testRunner/unittests/services/textChanges.ts index 164073207b3..fa5e723419a 100644 --- a/src/testRunner/unittests/services/textChanges.ts +++ b/src/testRunner/unittests/services/textChanges.ts @@ -2,7 +2,7 @@ // tslint:disable trim-trailing-whitespace namespace ts { - describe("textChanges", () => { + describe("services:: textChanges", () => { function findChild(name: string, n: Node) { return find(n)!; @@ -753,4 +753,4 @@ let x = foo }); } }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/transpile.ts b/src/testRunner/unittests/services/transpile.ts similarity index 97% rename from src/testRunner/unittests/transpile.ts rename to src/testRunner/unittests/services/transpile.ts index b545b76c3db..533f07884f4 100644 --- a/src/testRunner/unittests/transpile.ts +++ b/src/testRunner/unittests/services/transpile.ts @@ -1,5 +1,5 @@ namespace ts { - describe("Transpile", () => { + describe("services:: Transpile", () => { interface TranspileTestSettings { options?: TranspileOptions; diff --git a/src/testRunner/unittests/tscWatch/emit.ts b/src/testRunner/unittests/tscWatch/emit.ts index e0e9abe516c..2f7a70bd2c6 100644 --- a/src/testRunner/unittests/tscWatch/emit.ts +++ b/src/testRunner/unittests/tscWatch/emit.ts @@ -38,7 +38,7 @@ namespace ts.tscWatch { checkOutputDoesNotContain(host, expectedNonAffectedFiles); } - describe("tsc-watch emit with outFile or out setting", () => { + describe("tsc-watch:: emit with outFile or out setting", () => { function createWatchForOut(out?: string, outFile?: string) { const host = createWatchedSystem([]); const config: FileOrFolderEmit = { @@ -161,7 +161,7 @@ namespace ts.tscWatch { }); }); - describe("tsc-watch emit for configured projects", () => { + describe("tsc-watch:: emit for configured projects", () => { const file1Consumer1Path = "/a/b/file1Consumer1.ts"; const moduleFile1Path = "/a/b/moduleFile1.ts"; const configFilePath = "/a/b/tsconfig.json"; @@ -495,7 +495,7 @@ namespace ts.tscWatch { }); }); - describe("tsc-watch emit file content", () => { + describe("tsc-watch:: emit file content", () => { interface EmittedFile extends File { shouldBeWritten: boolean; } @@ -676,7 +676,7 @@ namespace ts.tscWatch { }); }); - describe("tsc-watch with when module emit is specified as node", () => { + describe("tsc-watch:: emit with when module emit is specified as node", () => { it("when instead of filechanged recursive directory watcher is invoked", () => { const configFile: File = { path: "/a/rootFolder/project/tsconfig.json", diff --git a/src/testRunner/unittests/tscWatch/helpers.ts b/src/testRunner/unittests/tscWatch/helpers.ts index f51ecc4e64d..46dc94e20e8 100644 --- a/src/testRunner/unittests/tscWatch/helpers.ts +++ b/src/testRunner/unittests/tscWatch/helpers.ts @@ -12,6 +12,15 @@ namespace ts.tscWatch { export import checkOutputContains = TestFSWithWatch.checkOutputContains; export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; + export const commonFile1: File = { + path: "/a/b/commonFile1.ts", + content: "let x = 1" + }; + export const commonFile2: File = { + path: "/a/b/commonFile2.ts", + content: "let y = 1" + }; + export function checkProgramActualFiles(program: Program, expectedFiles: ReadonlyArray) { checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); } diff --git a/src/testRunner/unittests/tscWatch/resolutionCache.ts b/src/testRunner/unittests/tscWatch/resolutionCache.ts index 759c80e93a6..e7b546edcd9 100644 --- a/src/testRunner/unittests/tscWatch/resolutionCache.ts +++ b/src/testRunner/unittests/tscWatch/resolutionCache.ts @@ -1,5 +1,5 @@ namespace ts.tscWatch { - describe("resolutionCache:: tsc-watch module resolution caching", () => { + describe("tsc-watch:: resolutionCache:: tsc-watch module resolution caching", () => { it("works", () => { const root = { path: "/a/d/f0.ts", @@ -404,7 +404,7 @@ declare module "fs" { }); }); - describe("resolutionCache:: tsc-watch with modules linked to sibling folder", () => { + describe("tsc-watch:: resolutionCache:: tsc-watch with modules linked to sibling folder", () => { const projectRoot = "/user/username/projects/project"; const mainPackageRoot = `${projectRoot}/main`; const linkedPackageRoot = `${projectRoot}/linked-package`; diff --git a/src/testRunner/unittests/tscWatch/watchApi.ts b/src/testRunner/unittests/tscWatch/watchApi.ts index 60072e24730..334b353f1d5 100644 --- a/src/testRunner/unittests/tscWatch/watchApi.ts +++ b/src/testRunner/unittests/tscWatch/watchApi.ts @@ -1,5 +1,5 @@ namespace ts.tscWatch { - describe("watchAPI:: tsc-watch with custom module resolution", () => { + describe("tsc-watch:: watchAPI:: tsc-watch with custom module resolution", () => { const projectRoot = "/user/username/projects/project"; const configFileJson: any = { compilerOptions: { module: "commonjs", resolveJsonModule: true }, diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts index 646fb59ef09..3d09e2a38f6 100644 --- a/src/testRunner/unittests/tscWatch/watchEnvironment.ts +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -1,6 +1,6 @@ namespace ts.tscWatch { import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; - describe("watchEnvironment:: tsc-watch with different polling/non polling options", () => { + describe("tsc-watch:: watchEnvironment:: tsc-watch with different polling/non polling options", () => { it("watchFile using dynamic priority polling", () => { const projectFolder = "/a/username/project"; const file1: File = { diff --git a/src/testRunner/unittests/tscWatchMode.ts b/src/testRunner/unittests/tscWatchMode.ts index 29726b12424..4d71599a1b5 100644 --- a/src/testRunner/unittests/tscWatchMode.ts +++ b/src/testRunner/unittests/tscWatchMode.ts @@ -25,15 +25,6 @@ namespace ts.tscWatch { } describe("tsc-watch program updates", () => { - const commonFile1: File = { - path: "/a/b/commonFile1.ts", - content: "let x = 1" - }; - const commonFile2: File = { - path: "/a/b/commonFile2.ts", - content: "let y = 1" - }; - it("create watch without config file", () => { const appFile: File = { path: "/a/b/c/app.ts", diff --git a/src/testRunner/unittests/tsserver/cancellationToken.ts b/src/testRunner/unittests/tsserver/cancellationToken.ts new file mode 100644 index 00000000000..5d9cd5527e6 --- /dev/null +++ b/src/testRunner/unittests/tsserver/cancellationToken.ts @@ -0,0 +1,271 @@ +namespace ts.projectSystem { + describe("tsserver:: cancellationToken", () => { + // Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test + let oldPrepare: AnyFunction; + before(() => { + oldPrepare = (Error as any).prepareStackTrace; + delete (Error as any).prepareStackTrace; + }); + + after(() => { + (Error as any).prepareStackTrace = oldPrepare; + }); + + it("is attached to request", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let xyz = 1;" + }; + const host = createServerHost([f1]); + let expectedRequestId: number; + const cancellationToken: server.ServerCancellationToken = { + isCancellationRequested: () => false, + setRequest: requestId => { + if (expectedRequestId === undefined) { + assert.isTrue(false, "unexpected call"); + } + assert.equal(requestId, expectedRequestId); + }, + resetRequest: noop + }; + + const session = createSession(host, { cancellationToken }); + + expectedRequestId = session.getNextSeq(); + session.executeCommandSeq({ + command: "open", + arguments: { file: f1.path } + }); + + expectedRequestId = session.getNextSeq(); + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + }); + + expectedRequestId = session.getNextSeq(); + session.executeCommandSeq({ + command: "occurrences", + arguments: { file: f1.path, line: 1, offset: 6 } + }); + + expectedRequestId = 2; + host.runQueuedImmediateCallbacks(); + expectedRequestId = 2; + host.runQueuedImmediateCallbacks(); + }); + + it("Geterr is cancellable", () => { + const f1 = { + path: "/a/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {} + }) + }; + + const cancellationToken = new TestServerCancellationToken(); + const host = createServerHost([f1, config]); + const session = createSession(host, { + canUseEvents: true, + eventHandler: noop, + cancellationToken + }); + { + session.executeCommandSeq({ + command: "open", + arguments: { file: f1.path } + }); + // send geterr for missing file + session.executeCommandSeq({ + command: "geterr", + arguments: { files: ["/a/missing"] } + }); + // no files - expect 'completed' event + assert.equal(host.getOutput().length, 1, "expect 1 message"); + verifyRequestCompleted(session.getSeq(), 0); + } + { + const getErrId = session.getNextSeq(); + // send geterr for a valid file + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + }); + + assert.equal(host.getOutput().length, 0, "expect 0 messages"); + + // run new request + session.executeCommandSeq({ + command: "projectInfo", + arguments: { file: f1.path } + }); + session.clearMessages(); + + // cancel previously issued Geterr + cancellationToken.setRequestToCancel(getErrId); + host.runQueuedTimeoutCallbacks(); + + assert.equal(host.getOutput().length, 1, "expect 1 message"); + verifyRequestCompleted(getErrId, 0); + + cancellationToken.resetToken(); + } + { + const getErrId = session.getNextSeq(); + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + }); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); + + // run first step + host.runQueuedTimeoutCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + const e1 = getMessage(0); + assert.equal(e1.event, "syntaxDiag"); + session.clearMessages(); + + cancellationToken.setRequestToCancel(getErrId); + host.runQueuedImmediateCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + verifyRequestCompleted(getErrId, 0); + + cancellationToken.resetToken(); + } + { + const getErrId = session.getNextSeq(); + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + }); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); + + // run first step + host.runQueuedTimeoutCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + const e1 = getMessage(0); + assert.equal(e1.event, "syntaxDiag"); + session.clearMessages(); + + // the semanticDiag message + host.runQueuedImmediateCallbacks(); + assert.equal(host.getOutput().length, 1); + const e2 = getMessage(0); + assert.equal(e2.event, "semanticDiag"); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + assert.equal(host.getOutput().length, 2); + const e3 = getMessage(0); + assert.equal(e3.event, "suggestionDiag"); + verifyRequestCompleted(getErrId, 1); + + cancellationToken.resetToken(); + } + { + const getErr1 = session.getNextSeq(); + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + }); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); + // run first step + host.runQueuedTimeoutCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + const e1 = getMessage(0); + assert.equal(e1.event, "syntaxDiag"); + session.clearMessages(); + + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + }); + // make sure that getErr1 is completed + verifyRequestCompleted(getErr1, 0); + } + + function verifyRequestCompleted(expectedSeq: number, n: number) { + const event = getMessage(n); + assert.equal(event.event, "requestCompleted"); + assert.equal(event.body.request_seq, expectedSeq, "expectedSeq"); + session.clearMessages(); + } + + function getMessage(n: number) { + return JSON.parse(server.extractMessage(host.getOutput()[n])); + } + }); + + it("Lower priority tasks are cancellable", () => { + const f1 = { + path: "/a/app.ts", + content: `{ let x = 1; } var foo = "foo"; var bar = "bar"; var fooBar = "fooBar";` + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {} + }) + }; + const cancellationToken = new TestServerCancellationToken(/*cancelAfterRequest*/ 3); + const host = createServerHost([f1, config]); + const session = createSession(host, { + canUseEvents: true, + eventHandler: noop, + cancellationToken, + throttleWaitMilliseconds: 0 + }); + { + session.executeCommandSeq({ + command: "open", + arguments: { file: f1.path } + }); + + // send navbar request (normal priority) + session.executeCommandSeq({ + command: "navbar", + arguments: { file: f1.path } + }); + + // ensure the nav bar request can be canceled + verifyExecuteCommandSeqIsCancellable({ + command: "navbar", + arguments: { file: f1.path } + }); + + // send outlining spans request (normal priority) + session.executeCommandSeq({ + command: "outliningSpans", + arguments: { file: f1.path } + }); + + // ensure the outlining spans request can be canceled + verifyExecuteCommandSeqIsCancellable({ + command: "outliningSpans", + arguments: { file: f1.path } + }); + } + + function verifyExecuteCommandSeqIsCancellable(request: Partial) { + // Set the next request to be cancellable + // The cancellation token will cancel the request the third time + // isCancellationRequested() is called. + cancellationToken.setRequestToCancel(session.getNextSeq()); + let operationCanceledExceptionThrown = false; + + try { + session.executeCommandSeq(request); + } + catch (e) { + assert(e instanceof OperationCanceledException); + operationCanceledExceptionThrown = true; + } + assert(operationCanceledExceptionThrown, "Operation Canceled Exception not thrown for request: " + JSON.stringify(request)); + } + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/completions.ts b/src/testRunner/unittests/tsserver/completions.ts new file mode 100644 index 00000000000..ed020bae0ba --- /dev/null +++ b/src/testRunner/unittests/tsserver/completions.ts @@ -0,0 +1,122 @@ +namespace ts.projectSystem { + describe("tsserver:: completions", () => { + it("works", () => { + const aTs: File = { + path: "/a.ts", + content: "export const foo = 0;", + }; + const bTs: File = { + path: "/b.ts", + content: "foo", + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + + const session = createSession(createServerHost([aTs, bTs, tsconfig])); + openFilesForSession([aTs, bTs], session); + + const requestLocation: protocol.FileLocationRequestArgs = { + file: bTs.path, + line: 1, + offset: 3, + }; + + const response = executeSessionRequest(session, protocol.CommandTypes.CompletionInfo, { + ...requestLocation, + includeExternalModuleExports: true, + prefix: "foo", + }); + const entry: protocol.CompletionEntry = { + hasAction: true, + insertText: undefined, + isRecommended: undefined, + kind: ScriptElementKind.constElement, + kindModifiers: ScriptElementKindModifier.exportedModifier, + name: "foo", + replacementSpan: undefined, + sortText: "0", + source: "/a", + }; + assert.deepEqual(response, { + isGlobalCompletion: true, + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: [entry], + }); + + const detailsRequestArgs: protocol.CompletionDetailsRequestArgs = { + ...requestLocation, + entryNames: [{ name: "foo", source: "/a" }], + }; + + const detailsResponse = executeSessionRequest(session, protocol.CommandTypes.CompletionDetails, detailsRequestArgs); + const detailsCommon: protocol.CompletionEntryDetails & CompletionEntryDetails = { + displayParts: [ + keywordPart(SyntaxKind.ConstKeyword), + spacePart(), + displayPart("foo", SymbolDisplayPartKind.localName), + punctuationPart(SyntaxKind.ColonToken), + spacePart(), + displayPart("0", SymbolDisplayPartKind.stringLiteral), + ], + documentation: emptyArray, + kind: ScriptElementKind.constElement, + kindModifiers: ScriptElementKindModifier.exportedModifier, + name: "foo", + source: [{ text: "./a", kind: "text" }], + tags: undefined, + }; + assert.deepEqual | undefined>(detailsResponse, [ + { + codeActions: [ + { + description: `Import 'foo' from module "./a"`, + changes: [ + { + fileName: "/b.ts", + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: 'import { foo } from "./a";\n\n', + }, + ], + }, + ], + commands: undefined, + }, + ], + ...detailsCommon, + }, + ]); + + interface CompletionDetailsFullRequest extends protocol.FileLocationRequest { + readonly command: protocol.CommandTypes.CompletionDetailsFull; + readonly arguments: protocol.CompletionDetailsRequestArgs; + } + interface CompletionDetailsFullResponse extends protocol.Response { + readonly body?: ReadonlyArray; + } + const detailsFullResponse = executeSessionRequest(session, protocol.CommandTypes.CompletionDetailsFull, detailsRequestArgs); + assert.deepEqual | undefined>(detailsFullResponse, [ + { + codeActions: [ + { + description: `Import 'foo' from module "./a"`, + changes: [ + { + fileName: "/b.ts", + textChanges: [createTextChange(createTextSpan(0, 0), 'import { foo } from "./a";\n\n')], + }, + ], + commands: undefined, + } + ], + ...detailsCommon, + } + ]); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/configFileSearch.ts b/src/testRunner/unittests/tsserver/configFileSearch.ts new file mode 100644 index 00000000000..4d0b0c0efdd --- /dev/null +++ b/src/testRunner/unittests/tsserver/configFileSearch.ts @@ -0,0 +1,174 @@ +namespace ts.projectSystem { + describe("tsserver:: searching for config file", () => { + it("should stop at projectRootPath if given", () => { + const f1 = { + path: "/a/file1.ts", + content: "" + }; + const configFile = { + path: "/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, configFile]); + const service = createProjectService(host); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a"); + + checkNumberOfConfiguredProjects(service, 0); + checkNumberOfInferredProjects(service, 1); + + service.closeClientFile(f1.path); + service.openClientFile(f1.path); + checkNumberOfConfiguredProjects(service, 1); + checkNumberOfInferredProjects(service, 0); + }); + + it("should use projectRootPath when searching for inferred project again", () => { + const projectDir = "/a/b/projects/project"; + const configFileLocation = `${projectDir}/src`; + const f1 = { + path: `${configFileLocation}/file1.ts`, + content: "" + }; + const configFile = { + path: `${configFileLocation}/tsconfig.json`, + content: "{}" + }; + const configFile2 = { + path: "/a/b/projects/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, libFile, configFile, configFile2]); + const service = createProjectService(host); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); + checkNumberOfProjects(service, { configuredProjects: 1 }); + assert.isDefined(service.configuredProjects.get(configFile.path)); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, [], /*recursive*/ false); + const typeRootLocations = getTypeRootsFromLocation(configFileLocation); + checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true); + + // Delete config file - should create inferred project and not configured project + host.reloadFS([f1, libFile, configFile2]); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true); + }); + + it("should use projectRootPath when searching for inferred project again 2", () => { + const projectDir = "/a/b/projects/project"; + const configFileLocation = `${projectDir}/src`; + const f1 = { + path: `${configFileLocation}/file1.ts`, + content: "" + }; + const configFile = { + path: `${configFileLocation}/tsconfig.json`, + content: "{}" + }; + const configFile2 = { + path: "/a/b/projects/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, libFile, configFile, configFile2]); + const service = createProjectService(host, { useSingleInferredProject: true }, { useInferredProjectPerProjectRoot: true }); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); + checkNumberOfProjects(service, { configuredProjects: 1 }); + assert.isDefined(service.configuredProjects.get(configFile.path)); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true); + + // Delete config file - should create inferred project with project root path set + host.reloadFS([f1, libFile, configFile2]); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(service, { inferredProjects: 1 }); + assert.equal(service.inferredProjects[0].projectRootPath, projectDir); + checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true); + }); + + describe("when the opened file is not from project root", () => { + const projectRoot = "/a/b/projects/project"; + const file: File = { + path: `${projectRoot}/src/index.ts`, + content: "let y = 10" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const files = [file, libFile]; + const filesWithConfig = files.concat(tsconfig); + const dirOfFile = getDirectoryPath(file.path); + + function openClientFile(files: File[]) { + const host = createServerHost(files); + const projectService = createProjectService(host); + + projectService.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a/b/projects/proj"); + return { host, projectService }; + } + + function verifyConfiguredProject(host: TestServerHost, projectService: TestProjectService, orphanInferredProject?: boolean) { + projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: orphanInferredProject ? 1 : 0 }); + const project = Debug.assertDefined(projectService.configuredProjects.get(tsconfig.path)); + + if (orphanInferredProject) { + const inferredProject = projectService.inferredProjects[0]; + assert.isTrue(inferredProject.isOrphan()); + } + + checkProjectActualFiles(project, [file.path, libFile.path, tsconfig.path]); + checkWatchedFiles(host, [libFile.path, tsconfig.path]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, (orphanInferredProject ? [projectRoot, `${dirOfFile}/node_modules/@types`] : [projectRoot]).concat(getTypeRootsFromLocation(projectRoot)), /*recursive*/ true); + } + + function verifyInferredProject(host: TestServerHost, projectService: TestProjectService) { + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + assert.isDefined(project); + + const filesToWatch = [libFile.path]; + forEachAncestorDirectory(dirOfFile, ancestor => { + filesToWatch.push(combinePaths(ancestor, "tsconfig.json")); + filesToWatch.push(combinePaths(ancestor, "jsconfig.json")); + }); + + checkProjectActualFiles(project, [file.path, libFile.path]); + checkWatchedFiles(host, filesToWatch); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, getTypeRootsFromLocation(dirOfFile), /*recursive*/ true); + } + + it("tsconfig for the file exists", () => { + const { host, projectService } = openClientFile(filesWithConfig); + verifyConfiguredProject(host, projectService); + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyInferredProject(host, projectService); + + host.reloadFS(filesWithConfig); + host.runQueuedTimeoutCallbacks(); + verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); + }); + + it("tsconfig for the file does not exist", () => { + const { host, projectService } = openClientFile(files); + verifyInferredProject(host, projectService); + + host.reloadFS(filesWithConfig); + host.runQueuedTimeoutCallbacks(); + verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyInferredProject(host, projectService); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/declarationFileMaps.ts b/src/testRunner/unittests/tsserver/declarationFileMaps.ts new file mode 100644 index 00000000000..334af82ca48 --- /dev/null +++ b/src/testRunner/unittests/tsserver/declarationFileMaps.ts @@ -0,0 +1,566 @@ +namespace ts.projectSystem { + function protocolFileSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileSpan { + return { file: file.path, ...protocolTextSpanFromSubstring(file.content, substring, options) }; + } + + function documentSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): DocumentSpan { + return { fileName: file.path, textSpan: textSpanFromSubstring(file.content, substring, options) }; + } + + function renameLocation(file: File, substring: string, options?: SpanFromSubstringOptions): RenameLocation { + return documentSpanFromSubstring(file, substring, options); + } + + function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem { + return { + ...protocolFileSpanFromSubstring(file, text, options), + isDefinition, + isWriteAccess: isDefinition, + lineText, + }; + } + + function makeReferenceEntry(file: File, isDefinition: boolean, text: string, options?: SpanFromSubstringOptions): ReferenceEntry { + return { + ...documentSpanFromSubstring(file, text, options), + isDefinition, + isWriteAccess: isDefinition, + isInString: undefined, + }; + } + + function checkDeclarationFiles(file: File, session: TestSession, expectedFiles: ReadonlyArray): void { + openFilesForSession([file], session); + const project = Debug.assertDefined(session.getProjectService().getDefaultProjectForFile(file.path as server.NormalizedPath, /*ensureProject*/ false)); + const program = project.getCurrentProgram()!; + const output = getFileEmitOutput(program, Debug.assertDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true); + closeFilesForSession([file], session); + + Debug.assert(!output.emitSkipped); + assert.deepEqual(output.outputFiles, expectedFiles.map((e): OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false }))); + } + + describe("tsserver:: with declaration file maps:: project references", () => { + const aTs: File = { + path: "/a/a.ts", + content: "export function fnA() {}\nexport interface IfaceA {}\nexport const instanceA: IfaceA = {};", + }; + const compilerOptions: CompilerOptions = { + outDir: "bin", + declaration: true, + declarationMap: true, + composite: true, + }; + const configContent = JSON.stringify({ compilerOptions }); + const aTsconfig: File = { path: "/a/tsconfig.json", content: configContent }; + + const aDtsMapContent: RawSourceMap = { + version: 3, + file: "a.d.ts", + sourceRoot: "", + sources: ["../a.ts"], + names: [], + mappings: "AAAA,wBAAgB,GAAG,SAAK;AACxB,MAAM,WAAW,MAAM;CAAG;AAC1B,eAAO,MAAM,SAAS,EAAE,MAAW,CAAC" + }; + const aDtsMap: File = { + path: "/a/bin/a.d.ts.map", + content: JSON.stringify(aDtsMapContent), + }; + const aDts: File = { + path: "/a/bin/a.d.ts", + // Need to mangle the sourceMappingURL part or it breaks the build + content: `export declare function fnA(): void;\nexport interface IfaceA {\n}\nexport declare const instanceA: IfaceA;\n//# source${""}MappingURL=a.d.ts.map`, + }; + + const bTs: File = { + path: "/b/b.ts", + content: "export function fnB() {}", + }; + const bTsconfig: File = { path: "/b/tsconfig.json", content: configContent }; + + const bDtsMapContent: RawSourceMap = { + version: 3, + file: "b.d.ts", + sourceRoot: "", + sources: ["../b.ts"], + names: [], + mappings: "AAAA,wBAAgB,GAAG,SAAK", + }; + const bDtsMap: File = { + path: "/b/bin/b.d.ts.map", + content: JSON.stringify(bDtsMapContent), + }; + const bDts: File = { + // Need to mangle the sourceMappingURL part or it breaks the build + path: "/b/bin/b.d.ts", + content: `export declare function fnB(): void;\n//# source${""}MappingURL=b.d.ts.map`, + }; + + const dummyFile: File = { + path: "/dummy/dummy.ts", + content: "let a = 10;" + }; + + const userTs: File = { + path: "/user/user.ts", + content: 'import * as a from "../a/bin/a";\nimport * as b from "../b/bin/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }', + }; + + const userTsForConfigProject: File = { + path: "/user/user.ts", + content: 'import * as a from "../a/a";\nimport * as b from "../b/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }', + }; + + const userTsconfig: File = { + path: "/user/tsconfig.json", + content: JSON.stringify({ + file: ["user.ts"], + references: [{ path: "../a" }, { path: "../b" }] + }) + }; + + function makeSampleProjects(addUserTsConfig?: boolean) { + const host = createServerHost([aTs, aTsconfig, aDtsMap, aDts, bTsconfig, bTs, bDtsMap, bDts, ...(addUserTsConfig ? [userTsForConfigProject, userTsconfig] : [userTs]), dummyFile]); + const session = createSession(host); + + checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); + checkDeclarationFiles(bTs, session, [bDtsMap, bDts]); + + // Testing what happens if we delete the original sources. + host.deleteFile(bTs.path); + + openFilesForSession([userTs], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, addUserTsConfig ? { configuredProjects: 1 } : { inferredProjects: 1 }); + return session; + } + + function verifyInferredProjectUnchanged(session: TestSession) { + checkProjectActualFiles(session.getProjectService().inferredProjects[0], [userTs.path, aDts.path, bDts.path]); + } + + function verifyDummyProject(session: TestSession) { + checkProjectActualFiles(session.getProjectService().inferredProjects[0], [dummyFile.path]); + } + + function verifyOnlyOrphanInferredProject(session: TestSession) { + openFilesForSession([dummyFile], session); + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); + verifyDummyProject(session); + } + + function verifySingleInferredProject(session: TestSession) { + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); + verifyInferredProjectUnchanged(session); + + // Close user file should close all the projects after opening dummy file + closeFilesForSession([userTs], session); + verifyOnlyOrphanInferredProject(session); + } + + function verifyATsConfigProject(session: TestSession) { + checkProjectActualFiles(session.getProjectService().configuredProjects.get(aTsconfig.path)!, [aTs.path, aTsconfig.path]); + } + + function verifyATsConfigOriginalProject(session: TestSession) { + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyInferredProjectUnchanged(session); + verifyATsConfigProject(session); + // Close user file should close all the projects + closeFilesForSession([userTs], session); + verifyOnlyOrphanInferredProject(session); + } + + function verifyATsConfigWhenOpened(session: TestSession) { + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyInferredProjectUnchanged(session); + verifyATsConfigProject(session); + + closeFilesForSession([userTs], session); + openFilesForSession([dummyFile], session); + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyDummyProject(session); + verifyATsConfigProject(session); // ATsConfig should still be alive + } + + function verifyUserTsConfigProject(session: TestSession) { + checkProjectActualFiles(session.getProjectService().configuredProjects.get(userTsconfig.path)!, [userTs.path, aDts.path, userTsconfig.path]); + } + + it("goToDefinition", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.Definition, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "fnA")]); + verifySingleInferredProject(session); + }); + + it("getDefinitionAndBoundSpan", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), + definitions: [protocolFileSpanFromSubstring(aTs, "fnA")], + }); + verifySingleInferredProject(session); + }); + + it("getDefinitionAndBoundSpan with file navigation", () => { + const session = makeSampleProjects(/*addUserTsConfig*/ true); + const response = executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), + definitions: [protocolFileSpanFromSubstring(aTs, "fnA")], + }); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); + verifyUserTsConfigProject(session); + + // Navigate to the definition + closeFilesForSession([userTs], session); + openFilesForSession([aTs], session); + + // UserTs configured project should be alive + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + verifyUserTsConfigProject(session); + verifyATsConfigProject(session); + + closeFilesForSession([aTs], session); + verifyOnlyOrphanInferredProject(session); + }); + + it("goToType", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.TypeDefinition, protocolFileLocationFromSubstring(userTs, "instanceA")); + assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "IfaceA")]); + verifySingleInferredProject(session); + }); + + it("goToImplementation", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.Implementation, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "fnA")]); + verifySingleInferredProject(session); + }); + + it("goToDefinition -- target does not exist", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, CommandNames.Definition, protocolFileLocationFromSubstring(userTs, "fnB()")); + // bTs does not exist, so stick with bDts + assert.deepEqual(response, [protocolFileSpanFromSubstring(bDts, "fnB")]); + verifySingleInferredProject(session); + }); + + it("navigateTo", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, CommandNames.Navto, { file: userTs.path, searchValue: "fn" }); + assert.deepEqual | undefined>(response, [ + { + ...protocolFileSpanFromSubstring(bDts, "export declare function fnB(): void;"), + name: "fnB", + matchKind: "prefix", + isCaseSensitive: true, + kind: ScriptElementKind.functionElement, + kindModifiers: "export,declare", + }, + { + ...protocolFileSpanFromSubstring(userTs, "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"), + name: "fnUser", + matchKind: "prefix", + isCaseSensitive: true, + kind: ScriptElementKind.functionElement, + kindModifiers: "export", + }, + { + ...protocolFileSpanFromSubstring(aTs, "export function fnA() {}"), + name: "fnA", + matchKind: "prefix", + isCaseSensitive: true, + kind: ScriptElementKind.functionElement, + kindModifiers: "export", + }, + ]); + + verifyATsConfigOriginalProject(session); + }); + + const referenceATs = (aTs: File): protocol.ReferencesResponseItem => makeReferenceItem(aTs, /*isDefinition*/ true, "fnA", "export function fnA() {}"); + const referencesUserTs = (userTs: File): ReadonlyArray => [ + makeReferenceItem(userTs, /*isDefinition*/ false, "fnA", "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"), + ]; + + it("findAllReferences", () => { + const session = makeSampleProjects(); + + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + refs: [...referencesUserTs(userTs), referenceATs(aTs)], + symbolName: "fnA", + symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnA()").offset, + symbolDisplayString: "function fnA(): void", + }); + + verifyATsConfigOriginalProject(session); + }); + + it("findAllReferences -- starting at definition", () => { + const session = makeSampleProjects(); + openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(aTs, "fnA")); + assert.deepEqual(response, { + refs: [referenceATs(aTs), ...referencesUserTs(userTs)], + symbolName: "fnA", + symbolStartOffset: protocolLocationFromSubstring(aTs.content, "fnA").offset, + symbolDisplayString: "function fnA(): void", + }); + verifyATsConfigWhenOpened(session); + }); + + interface ReferencesFullRequest extends protocol.FileLocationRequest { readonly command: protocol.CommandTypes.ReferencesFull; } + interface ReferencesFullResponse extends protocol.Response { readonly body: ReadonlyArray; } + + it("findAllReferencesFull", () => { + const session = makeSampleProjects(); + + const responseFull = executeSessionRequest(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(userTs, "fnA()")); + + assert.deepEqual>(responseFull, [ + { + definition: { + ...documentSpanFromSubstring(aTs, "fnA"), + kind: ScriptElementKind.functionElement, + name: "function fnA(): void", + containerKind: ScriptElementKind.unknown, + containerName: "", + displayParts: [ + keywordPart(SyntaxKind.FunctionKeyword), + spacePart(), + displayPart("fnA", SymbolDisplayPartKind.functionName), + punctuationPart(SyntaxKind.OpenParenToken), + punctuationPart(SyntaxKind.CloseParenToken), + punctuationPart(SyntaxKind.ColonToken), + spacePart(), + keywordPart(SyntaxKind.VoidKeyword), + ], + }, + references: [ + makeReferenceEntry(userTs, /*isDefinition*/ false, "fnA"), + makeReferenceEntry(aTs, /*isDefinition*/ true, "fnA"), + ], + }, + ]); + verifyATsConfigOriginalProject(session); + }); + + it("findAllReferencesFull definition is in mapped file", () => { + const aTs: File = { path: "/a/a.ts", content: `function f() {}` }; + const aTsconfig: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ compilerOptions: { declaration: true, declarationMap: true, outFile: "../bin/a.js" } }), + }; + const bTs: File = { path: "/b/b.ts", content: `f();` }; + const bTsconfig: File = { path: "/b/tsconfig.json", content: JSON.stringify({ references: [{ path: "../a" }] }) }; + const aDts: File = { path: "/bin/a.d.ts", content: `declare function f(): void;\n//# sourceMappingURL=a.d.ts.map` }; + const aDtsMap: File = { + path: "/bin/a.d.ts.map", + content: JSON.stringify({ version: 3, file: "a.d.ts", sourceRoot: "", sources: ["../a/a.ts"], names: [], mappings: "AAAA,iBAAS,CAAC,SAAK" }), + }; + + const session = createSession(createServerHost([aTs, aTsconfig, bTs, bTsconfig, aDts, aDtsMap])); + checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); + openFilesForSession([bTs], session); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); + + const responseFull = executeSessionRequest(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(bTs, "f()")); + + assert.deepEqual>(responseFull, [ + { + definition: { + containerKind: ScriptElementKind.unknown, + containerName: "", + displayParts: [ + keywordPart(SyntaxKind.FunctionKeyword), + spacePart(), + displayPart("f", SymbolDisplayPartKind.functionName), + punctuationPart(SyntaxKind.OpenParenToken), + punctuationPart(SyntaxKind.CloseParenToken), + punctuationPart(SyntaxKind.ColonToken), + spacePart(), + keywordPart(SyntaxKind.VoidKeyword), + ], + fileName: aTs.path, + kind: ScriptElementKind.functionElement, + name: "function f(): void", + textSpan: { start: 9, length: 1 }, + }, + references: [ + { + fileName: bTs.path, + isDefinition: false, + isInString: undefined, + isWriteAccess: false, + textSpan: { start: 0, length: 1 }, + }, + { + fileName: aTs.path, + isDefinition: true, + isInString: undefined, + isWriteAccess: true, + textSpan: { start: 9, length: 1 }, + }, + ], + } + ]); + }); + + it("findAllReferences -- target does not exist", () => { + const session = makeSampleProjects(); + + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnB()")); + assert.deepEqual(response, { + refs: [ + makeReferenceItem(bDts, /*isDefinition*/ true, "fnB", "export declare function fnB(): void;"), + makeReferenceItem(userTs, /*isDefinition*/ false, "fnB", "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"), + ], + symbolName: "fnB", + symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnB()").offset, + symbolDisplayString: "function fnB(): void", + }); + verifySingleInferredProject(session); + }); + + const renameATs = (aTs: File): protocol.SpanGroup => ({ + file: aTs.path, + locs: [protocolRenameSpanFromSubstring(aTs.content, "fnA")], + }); + const renameUserTs = (userTs: File): protocol.SpanGroup => ({ + file: userTs.path, + locs: [protocolRenameSpanFromSubstring(userTs.content, "fnA")], + }); + + it("renameLocations", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "fnA", + fileToRename: undefined, + fullDisplayName: '"/a/bin/a".fnA', // Ideally this would use the original source's path instead of the declaration file's path. + kind: ScriptElementKind.functionElement, + kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","), + triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), + }, + locs: [renameUserTs(userTs), renameATs(aTs)], + }); + verifyATsConfigOriginalProject(session); + }); + + it("renameLocations -- starting at definition", () => { + const session = makeSampleProjects(); + openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. + const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "fnA")); + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "fnA", + fileToRename: undefined, + fullDisplayName: '"/a/a".fnA', + kind: ScriptElementKind.functionElement, + kindModifiers: ScriptElementKindModifier.exportedModifier, + triggerSpan: protocolTextSpanFromSubstring(aTs.content, "fnA"), + }, + locs: [renameATs(aTs), renameUserTs(userTs)], + }); + verifyATsConfigWhenOpened(session); + }); + + it("renameLocationsFull", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.RenameLocationsFull, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual>(response, [ + renameLocation(userTs, "fnA"), + renameLocation(aTs, "fnA"), + ]); + verifyATsConfigOriginalProject(session); + }); + + it("renameLocations -- target does not exist", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnB()")); + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "fnB", + fileToRename: undefined, + fullDisplayName: '"/b/bin/b".fnB', + kind: ScriptElementKind.functionElement, + kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","), + triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnB"), + }, + locs: [ + { + file: bDts.path, + locs: [protocolRenameSpanFromSubstring(bDts.content, "fnB")], + }, + { + file: userTs.path, + locs: [protocolRenameSpanFromSubstring(userTs.content, "fnB")], + }, + ], + }); + verifySingleInferredProject(session); + }); + + it("getEditsForFileRename", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.GetEditsForFileRename, { + oldFilePath: aTs.path, + newFilePath: "/a/aNew.ts", + }); + assert.deepEqual>(response, [ + { + fileName: userTs.path, + textChanges: [ + { ...protocolTextSpanFromSubstring(userTs.content, "../a/bin/a"), newText: "../a/bin/aNew" }, + ], + }, + ]); + verifySingleInferredProject(session); + }); + + it("getEditsForFileRename when referencing project doesnt include file and its renamed", () => { + const aTs: File = { path: "/a/src/a.ts", content: "" }; + const aTsconfig: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + composite: true, + declaration: true, + declarationMap: true, + outDir: "./build", + } + }), + }; + const bTs: File = { path: "/b/src/b.ts", content: "" }; + const bTsconfig: File = { + path: "/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "./build", + }, + include: ["./src"], + references: [{ path: "../a" }], + }), + }; + + const host = createServerHost([aTs, aTsconfig, bTs, bTsconfig]); + const session = createSession(host); + openFilesForSession([aTs, bTs], session); + const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { + oldFilePath: aTs.path, + newFilePath: "/a/src/a1.ts", + }); + assert.deepEqual>(response, []); // Should not change anything + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/documentRegistry.ts b/src/testRunner/unittests/tsserver/documentRegistry.ts new file mode 100644 index 00000000000..0fd812a89d2 --- /dev/null +++ b/src/testRunner/unittests/tsserver/documentRegistry.ts @@ -0,0 +1,94 @@ +namespace ts.projectSystem { + describe("tsserver:: document registry in project service", () => { + const projectRootPath = "/user/username/projects/project"; + const importModuleContent = `import {a} from "./module1"`; + const file: File = { + path: `${projectRootPath}/index.ts`, + content: importModuleContent + }; + const moduleFile: File = { + path: `${projectRootPath}/module1.d.ts`, + content: "export const a: number;" + }; + const configFile: File = { + path: `${projectRootPath}/tsconfig.json`, + content: JSON.stringify({ files: ["index.ts"] }) + }; + + function getProject(service: TestProjectService) { + return service.configuredProjects.get(configFile.path)!; + } + + function checkProject(service: TestProjectService, moduleIsOrphan: boolean) { + // Update the project + const project = getProject(service); + project.getLanguageService(); + checkProjectActualFiles(project, [file.path, libFile.path, configFile.path, ...(moduleIsOrphan ? [] : [moduleFile.path])]); + const moduleInfo = service.getScriptInfo(moduleFile.path)!; + assert.isDefined(moduleInfo); + assert.equal(moduleInfo.isOrphan(), moduleIsOrphan); + const key = service.documentRegistry.getKeyForCompilationSettings(project.getCompilationSettings()); + assert.deepEqual(service.documentRegistry.getLanguageServiceRefCounts(moduleInfo.path), [[key, moduleIsOrphan ? undefined : 1]]); + } + + function createServiceAndHost() { + const host = createServerHost([file, moduleFile, libFile, configFile]); + const service = createProjectService(host); + service.openClientFile(file.path); + checkProject(service, /*moduleIsOrphan*/ false); + return { host, service }; + } + + function changeFileToNotImportModule(service: TestProjectService) { + const info = service.getScriptInfo(file.path)!; + service.applyChangesToFile(info, [{ span: { start: 0, length: importModuleContent.length }, newText: "" }]); + checkProject(service, /*moduleIsOrphan*/ true); + } + + function changeFileToImportModule(service: TestProjectService) { + const info = service.getScriptInfo(file.path)!; + service.applyChangesToFile(info, [{ span: { start: 0, length: 0 }, newText: importModuleContent }]); + checkProject(service, /*moduleIsOrphan*/ false); + } + + it("Caches the source file if script info is orphan", () => { + const { service } = createServiceAndHost(); + const project = getProject(service); + + const moduleInfo = service.getScriptInfo(moduleFile.path)!; + const sourceFile = moduleInfo.cacheSourceFile!.sourceFile; + assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); + + // edit file + changeFileToNotImportModule(service); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); + + // write content back + changeFileToImportModule(service); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); + assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); + }); + + it("Caches the source file if script info is orphan, and orphan script info changes", () => { + const { host, service } = createServiceAndHost(); + const project = getProject(service); + + const moduleInfo = service.getScriptInfo(moduleFile.path)!; + const sourceFile = moduleInfo.cacheSourceFile!.sourceFile; + assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); + + // edit file + changeFileToNotImportModule(service); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); + + const updatedModuleContent = moduleFile.content + "\nexport const b: number;"; + host.writeFile(moduleFile.path, updatedModuleContent); + + // write content back + changeFileToImportModule(service); + assert.notEqual(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); + assert.equal(project.getSourceFile(moduleInfo.path), moduleInfo.cacheSourceFile!.sourceFile); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile.text, updatedModuleContent); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/duplicatePackages.ts b/src/testRunner/unittests/tsserver/duplicatePackages.ts new file mode 100644 index 00000000000..f8a282ee7d2 --- /dev/null +++ b/src/testRunner/unittests/tsserver/duplicatePackages.ts @@ -0,0 +1,54 @@ +namespace ts.projectSystem { + describe("tsserver:: duplicate packages", () => { + // Tests that 'moduleSpecifiers.ts' will import from the redirecting file, and not from the file it redirects to, if that can provide a global module specifier. + it("works with import fixes", () => { + const packageContent = "export const foo: number;"; + const packageJsonContent = JSON.stringify({ name: "foo", version: "1.2.3" }); + const aFooIndex: File = { path: "/a/node_modules/foo/index.d.ts", content: packageContent }; + const aFooPackage: File = { path: "/a/node_modules/foo/package.json", content: packageJsonContent }; + const bFooIndex: File = { path: "/b/node_modules/foo/index.d.ts", content: packageContent }; + const bFooPackage: File = { path: "/b/node_modules/foo/package.json", content: packageJsonContent }; + + const userContent = 'import("foo");\nfoo'; + const aUser: File = { path: "/a/user.ts", content: userContent }; + const bUser: File = { path: "/b/user.ts", content: userContent }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + + const host = createServerHost([aFooIndex, aFooPackage, bFooIndex, bFooPackage, aUser, bUser, tsconfig]); + const session = createSession(host); + + openFilesForSession([aUser, bUser], session); + + for (const user of [aUser, bUser]) { + const response = executeSessionRequest(session, protocol.CommandTypes.GetCodeFixes, { + file: user.path, + startLine: 2, + startOffset: 1, + endLine: 2, + endOffset: 4, + errorCodes: [Diagnostics.Cannot_find_name_0.code], + }); + assert.deepEqual | undefined>(response, [ + { + description: `Import 'foo' from module "foo"`, + fixName: "import", + fixId: "fixMissingImport", + fixAllDescription: "Add all missing imports", + changes: [{ + fileName: user.path, + textChanges: [{ + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: 'import { foo } from "foo";\n\n', + }], + }], + commands: undefined, + }, + ]); + } + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts b/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts new file mode 100644 index 00000000000..d790a1aa25a --- /dev/null +++ b/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts @@ -0,0 +1,45 @@ +namespace ts.projectSystem { + describe("tsserver:: forceConsistentCasingInFileNames", () => { + it("works when extends is specified with a case insensitive file system", () => { + const rootPath = "/Users/username/dev/project"; + const file1: File = { + path: `${rootPath}/index.ts`, + content: 'import {x} from "file2";', + }; + const file2: File = { + path: `${rootPath}/file2.js`, + content: "", + }; + const file2Dts: File = { + path: `${rootPath}/types/file2/index.d.ts`, + content: "export declare const x: string;", + }; + const tsconfigAll: File = { + path: `${rootPath}/tsconfig.all.json`, + content: JSON.stringify({ + compilerOptions: { + baseUrl: ".", + paths: { file2: ["./file2.js"] }, + typeRoots: ["./types"], + forceConsistentCasingInFileNames: true, + }, + }), + }; + const tsconfig: File = { + path: `${rootPath}/tsconfig.json`, + content: JSON.stringify({ extends: "./tsconfig.all.json" }), + }; + + const host = createServerHost([file1, file2, file2Dts, libFile, tsconfig, tsconfigAll], { useCaseSensitiveFileNames: false }); + const session = createSession(host); + + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + const diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/formatSettings.ts b/src/testRunner/unittests/tsserver/formatSettings.ts new file mode 100644 index 00000000000..4e3e2ebd418 --- /dev/null +++ b/src/testRunner/unittests/tsserver/formatSettings.ts @@ -0,0 +1,39 @@ +namespace ts.projectSystem { + describe("tsserver:: format settings", () => { + it("can be set globally", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x;" + }; + const host = createServerHost([f1]); + const projectService = createProjectService(host); + projectService.openClientFile(f1.path); + + const defaultSettings = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath); + + // set global settings + const newGlobalSettings1 = { ...defaultSettings, placeOpenBraceOnNewLineForControlBlocks: !defaultSettings.placeOpenBraceOnNewLineForControlBlocks }; + projectService.setHostConfiguration({ formatOptions: newGlobalSettings1 }); + + // get format options for file - should be equal to new global settings + const s1 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path)); + assert.deepEqual(s1, newGlobalSettings1, "file settings should be the same with global settings"); + + // set per file format options + const newPerFileSettings = { ...defaultSettings, insertSpaceAfterCommaDelimiter: !defaultSettings.insertSpaceAfterCommaDelimiter }; + projectService.setHostConfiguration({ formatOptions: newPerFileSettings, file: f1.path }); + + // get format options for file - should be equal to new per-file settings + const s2 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path)); + assert.deepEqual(s2, newPerFileSettings, "file settings should be the same with per-file settings"); + + // set new global settings - they should not affect ones that were set per-file + const newGlobalSettings2 = { ...defaultSettings, insertSpaceAfterSemicolonInForStatements: !defaultSettings.insertSpaceAfterSemicolonInForStatements }; + projectService.setHostConfiguration({ formatOptions: newGlobalSettings2 }); + + // get format options for file - should be equal to new per-file settings + const s3 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path)); + assert.deepEqual(s3, newPerFileSettings, "file settings should still be the same with per-file settings"); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/getEditsForFileRename.ts b/src/testRunner/unittests/tsserver/getEditsForFileRename.ts new file mode 100644 index 00000000000..3fadf99c3f8 --- /dev/null +++ b/src/testRunner/unittests/tsserver/getEditsForFileRename.ts @@ -0,0 +1,105 @@ +namespace ts.projectSystem { + describe("tsserver:: getEditsForFileRename", () => { + it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => { + const userTs: File = { + path: "/user.ts", + content: 'import { x } from "./old";', + }; + const newTs: File = { + path: "/new.ts", + content: "export const x = 0;", + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + + const host = createServerHost([userTs, newTs, tsconfig]); + const projectService = createProjectService(host); + projectService.openClientFile(userTs.path); + const project = projectService.configuredProjects.get(tsconfig.path)!; + + Debug.assert(!!project.resolveModuleNames); + + const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatSettings, emptyOptions); + assert.deepEqual>(edits, [{ + fileName: "/user.ts", + textChanges: [{ + span: textSpanFromSubstring(userTs.content, "./old"), + newText: "./new", + }], + }]); + }); + + it("works with multiple projects", () => { + const aUserTs: File = { + path: "/a/user.ts", + content: 'import { x } from "./old";', + }; + const aOldTs: File = { + path: "/a/old.ts", + content: "export const x = 0;", + }; + const aTsconfig: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ files: ["./old.ts", "./user.ts"] }), + }; + const bUserTs: File = { + path: "/b/user.ts", + content: 'import { x } from "../a/old";', + }; + const bTsconfig: File = { + path: "/b/tsconfig.json", + content: "{}", + }; + + const host = createServerHost([aUserTs, aOldTs, aTsconfig, bUserTs, bTsconfig]); + const session = createSession(host); + openFilesForSession([aUserTs, bUserTs], session); + + const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { + oldFilePath: aOldTs.path, + newFilePath: "/a/new.ts", + }); + assert.deepEqual>(response, [ + { + fileName: aTsconfig.path, + textChanges: [{ ...protocolTextSpanFromSubstring(aTsconfig.content, "./old.ts"), newText: "new.ts" }], + }, + { + fileName: aUserTs.path, + textChanges: [{ ...protocolTextSpanFromSubstring(aUserTs.content, "./old"), newText: "./new" }], + }, + { + fileName: bUserTs.path, + textChanges: [{ ...protocolTextSpanFromSubstring(bUserTs.content, "../a/old"), newText: "../a/new" }], + }, + ]); + }); + + it("works with file moved to inferred project", () => { + const aTs: File = { path: "/a.ts", content: 'import {} from "./b";' }; + const cTs: File = { path: "/c.ts", content: "export {};" }; + const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./a.ts", "./b.ts"] }) }; + + const host = createServerHost([aTs, cTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs, cTs], session); + + const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { + oldFilePath: "/b.ts", + newFilePath: cTs.path, + }); + assert.deepEqual>(response, [ + { + fileName: "/tsconfig.json", + textChanges: [{ ...protocolTextSpanFromSubstring(tsconfig.content, "./b.ts"), newText: "c.ts" }], + }, + { + fileName: "/a.ts", + textChanges: [{ ...protocolTextSpanFromSubstring(aTs.content, "./b"), newText: "./c" }], + }, + ]); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/helpers.ts b/src/testRunner/unittests/tsserver/helpers.ts index 86dfddf3bb3..1db6a9b36c8 100644 --- a/src/testRunner/unittests/tsserver/helpers.ts +++ b/src/testRunner/unittests/tsserver/helpers.ts @@ -15,6 +15,9 @@ namespace ts.projectSystem { export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; + export import commonFile1 = tscWatch.commonFile1; + export import commonFile2 = tscWatch.commonFile2; + const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; export function mapOutputToJson(s: string) { return convertToObject( @@ -459,9 +462,13 @@ namespace ts.projectSystem { return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes); } - //function checkOpenFiles(projectService: server.ProjectService, expectedFiles: File[]) { - // checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path)); - //} + export function checkOpenFiles(projectService: server.ProjectService, expectedFiles: File[]) { + checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path)); + } + + export function checkScriptInfos(projectService: server.ProjectService, expectedFiles: ReadonlyArray) { + checkArray("ScriptInfos files", arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles); + } export function protocolLocationFromSubstring(str: string, substring: string): protocol.Location { const start = str.indexOf(substring); @@ -497,18 +504,10 @@ namespace ts.projectSystem { Debug.assert(start !== -1); return createTextSpan(start, substring.length); } - //function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs { - // return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) }; - //} - //function protocolFileSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileSpan { - // return { file: file.path, ...protocolTextSpanFromSubstring(file.content, substring, options) }; - //} - //function documentSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): DocumentSpan { - // return { fileName: file.path, textSpan: textSpanFromSubstring(file.content, substring, options) }; - //} - //function renameLocation(file: File, substring: string, options?: SpanFromSubstringOptions): RenameLocation { - // return documentSpanFromSubstring(file, substring, options); - //} + + export function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs { + return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) }; + } export interface SpanFromSubstringOptions { readonly index: number; @@ -648,33 +647,4 @@ namespace ts.projectSystem { assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs)); } } - - //function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem { - // return { - // ...protocolFileSpanFromSubstring(file, text, options), - // isDefinition, - // isWriteAccess: isDefinition, - // lineText, - // }; - //} - - //function makeReferenceEntry(file: File, isDefinition: boolean, text: string, options?: SpanFromSubstringOptions): ReferenceEntry { - // return { - // ...documentSpanFromSubstring(file, text, options), - // isDefinition, - // isWriteAccess: isDefinition, - // isInString: undefined, - // }; - //} - - //function checkDeclarationFiles(file: File, session: TestSession, expectedFiles: ReadonlyArray): void { - // openFilesForSession([file], session); - // const project = Debug.assertDefined(session.getProjectService().getDefaultProjectForFile(file.path as server.NormalizedPath, /*ensureProject*/ false)); - // const program = project.getCurrentProgram()!; - // const output = getFileEmitOutput(program, Debug.assertDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true); - // closeFilesForSession([file], session); - - // Debug.assert(!output.emitSkipped); - // assert.deepEqual(output.outputFiles, expectedFiles.map((e): OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false }))); - //} } diff --git a/src/testRunner/unittests/tsserver/importHelpers.ts b/src/testRunner/unittests/tsserver/importHelpers.ts new file mode 100644 index 00000000000..42e38aed122 --- /dev/null +++ b/src/testRunner/unittests/tsserver/importHelpers.ts @@ -0,0 +1,18 @@ +namespace ts.projectSystem { + describe("tsserver:: import helpers", () => { + it("should not crash in tsserver", () => { + const f1 = { + path: "/a/app.ts", + content: "export async function foo() { return 100; }" + }; + const tslib = { + path: "/a/node_modules/tslib/index.d.ts", + content: "" + }; + const host = createServerHost([f1, tslib]); + const service = createProjectService(host); + service.openExternalProject({ projectFileName: "p", rootFiles: [toExternalFile(f1.path)], options: { importHelpers: true } }); + service.checkNumberOfProjects({ externalProjects: 1 }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/inferredProjects.ts b/src/testRunner/unittests/tsserver/inferredProjects.ts new file mode 100644 index 00000000000..b22413c6ab3 --- /dev/null +++ b/src/testRunner/unittests/tsserver/inferredProjects.ts @@ -0,0 +1,229 @@ +namespace ts.projectSystem { + describe("tsserver:: Inferred projects", () => { + it("should support files without extensions", () => { + const f = { + path: "/a/compile", + content: "let x = 1" + }; + const host = createServerHost([f]); + const session = createSession(host); + session.executeCommand({ + seq: 1, + type: "request", + command: "compilerOptionsForInferredProjects", + arguments: { + options: { + allowJs: true + } + } + }); + session.executeCommand({ + seq: 2, + type: "request", + command: "open", + arguments: { + file: f.path, + fileContent: f.content, + scriptKindName: "JS" + } + }); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [f.path]); + }); + + it("inferred projects per project root", () => { + const file1 = { path: "/a/file1.ts", content: "let x = 1;", projectRootPath: "/a" }; + const file2 = { path: "/a/file2.ts", content: "let y = 2;", projectRootPath: "/a" }; + const file3 = { path: "/b/file2.ts", content: "let x = 3;", projectRootPath: "/b" }; + const file4 = { path: "/c/file3.ts", content: "let z = 4;" }; + const host = createServerHost([file1, file2, file3, file4]); + const session = createSession(host, { + useSingleInferredProject: true, + useInferredProjectPerProjectRoot: true + }); + session.executeCommand({ + seq: 1, + type: "request", + command: CommandNames.CompilerOptionsForInferredProjects, + arguments: { + options: { + allowJs: true, + target: ScriptTarget.ESNext + } + } + }); + session.executeCommand({ + seq: 2, + type: "request", + command: CommandNames.CompilerOptionsForInferredProjects, + arguments: { + options: { + allowJs: true, + target: ScriptTarget.ES2015 + }, + projectRootPath: "/b" + } + }); + session.executeCommand({ + seq: 3, + type: "request", + command: CommandNames.Open, + arguments: { + file: file1.path, + fileContent: file1.content, + scriptKindName: "JS", + projectRootPath: file1.projectRootPath + } + }); + session.executeCommand({ + seq: 4, + type: "request", + command: CommandNames.Open, + arguments: { + file: file2.path, + fileContent: file2.content, + scriptKindName: "JS", + projectRootPath: file2.projectRootPath + } + }); + session.executeCommand({ + seq: 5, + type: "request", + command: CommandNames.Open, + arguments: { + file: file3.path, + fileContent: file3.content, + scriptKindName: "JS", + projectRootPath: file3.projectRootPath + } + }); + session.executeCommand({ + seq: 6, + type: "request", + command: CommandNames.Open, + arguments: { + file: file4.path, + fileContent: file4.content, + scriptKindName: "JS" + } + }); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 3 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]); + checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); + assert.equal(projectService.inferredProjects[0].getCompilationSettings().target, ScriptTarget.ESNext); + assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ScriptTarget.ESNext); + assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ScriptTarget.ES2015); + }); + + function checkInferredProject(inferredProject: server.InferredProject, actualFiles: File[], target: ScriptTarget) { + checkProjectActualFiles(inferredProject, actualFiles.map(f => f.path)); + assert.equal(inferredProject.getCompilationSettings().target, target); + } + + function verifyProjectRootWithCaseSensitivity(useCaseSensitiveFileNames: boolean) { + const files: [File, File, File, File] = [ + { 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: [File[], 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); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/languageService.ts b/src/testRunner/unittests/tsserver/languageService.ts new file mode 100644 index 00000000000..6ee68e74c15 --- /dev/null +++ b/src/testRunner/unittests/tsserver/languageService.ts @@ -0,0 +1,19 @@ +namespace ts.projectSystem { + describe("tsserver:: Language service", () => { + it("should work correctly on case-sensitive file systems", () => { + const lib = { + path: "/a/Lib/lib.d.ts", + content: "let x: number" + }; + const f = { + path: "/a/b/app.ts", + content: "let x = 1;" + }; + const host = createServerHost([lib, f], { executingFilePath: "/a/Lib/tsc.js", useCaseSensitiveFileNames: true }); + const projectService = createProjectService(host); + projectService.openClientFile(f.path); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + projectService.inferredProjects[0].getLanguageService().getProgram(); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts b/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts new file mode 100644 index 00000000000..dd0b954824d --- /dev/null +++ b/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts @@ -0,0 +1,55 @@ +namespace ts.projectSystem { + describe("tsserver:: maxNodeModuleJsDepth for inferred projects", () => { + it("should be set to 2 if the project has js root files", () => { + const file1: File = { + path: "/a/b/file1.js", + content: `var t = require("test"); t.` + }; + const moduleFile: File = { + path: "/a/b/node_modules/test/index.js", + content: `var v = 10; module.exports = v;` + }; + + const host = createServerHost([file1, moduleFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + + let project = projectService.inferredProjects[0]; + let options = project.getCompilationSettings(); + assert.isTrue(options.maxNodeModuleJsDepth === 2); + + // Assert the option sticks + projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES2016 }); + project = projectService.inferredProjects[0]; + options = project.getCompilationSettings(); + assert.isTrue(options.maxNodeModuleJsDepth === 2); + }); + + it("should return to normal state when all js root files are removed from project", () => { + const file1 = { + path: "/a/file1.ts", + content: "let x =1;" + }; + const file2 = { + path: "/a/file2.js", + content: "let x =1;" + }; + + const host = createServerHost([file1, file2, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + + projectService.openClientFile(file1.path); + checkNumberOfInferredProjects(projectService, 1); + let project = projectService.inferredProjects[0]; + assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth); + + projectService.openClientFile(file2.path); + project = projectService.inferredProjects[0]; + assert.isTrue(project.getCompilationSettings().maxNodeModuleJsDepth === 2); + + projectService.closeClientFile(file2.path); + project = projectService.inferredProjects[0]; + assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/metadataInResponse.ts b/src/testRunner/unittests/tsserver/metadataInResponse.ts new file mode 100644 index 00000000000..783fce4dd16 --- /dev/null +++ b/src/testRunner/unittests/tsserver/metadataInResponse.ts @@ -0,0 +1,99 @@ +namespace ts.projectSystem { + describe("tsserver:: with metadata in response", () => { + const metadata = "Extra Info"; + function verifyOutput(host: TestServerHost, expectedResponse: protocol.Response) { + const output = host.getOutput().map(mapOutputToJson); + assert.deepEqual(output, [expectedResponse]); + host.clearOutput(); + } + + function verifyCommandWithMetadata(session: TestSession, host: TestServerHost, command: Partial, expectedResponseBody: U) { + command.seq = session.getSeq(); + command.type = "request"; + session.onMessage(JSON.stringify(command)); + verifyOutput(host, expectedResponseBody ? + { seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: true, body: expectedResponseBody, metadata } : + { seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: false, message: "No content available." } + ); + } + + const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; + const tsconfig: File = { + path: "/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { plugins: [{ name: "myplugin" }] } + }) + }; + function createHostWithPlugin(files: ReadonlyArray) { + const host = createServerHost(files); + host.require = (_initialPath, moduleName) => { + assert.equal(moduleName, "myplugin"); + return { + module: () => ({ + create(info: server.PluginCreateInfo) { + const proxy = Harness.LanguageService.makeDefaultProxy(info); + proxy.getCompletionsAtPosition = (filename, position, options) => { + const result = info.languageService.getCompletionsAtPosition(filename, position, options); + if (result) { + result.metadata = metadata; + } + return result; + }; + return proxy; + } + }), + error: undefined + }; + }; + return host; + } + + describe("With completion requests", () => { + const completionRequestArgs: protocol.CompletionsRequestArgs = { + file: aTs.path, + line: 1, + offset: aTs.content.indexOf("this.") + 1 + "this.".length + }; + const expectedCompletionEntries: ReadonlyArray = [ + { name: "foo", kind: ScriptElementKind.memberFunctionElement, kindModifiers: "", sortText: "0" }, + { name: "prop", kind: ScriptElementKind.memberVariableElement, kindModifiers: "", sortText: "0" } + ]; + + it("can pass through metadata when the command returns array", () => { + const host = createHostWithPlugin([aTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs], session); + verifyCommandWithMetadata>(session, host, { + command: protocol.CommandTypes.Completions, + arguments: completionRequestArgs + }, expectedCompletionEntries); + }); + + it("can pass through metadata when the command returns object", () => { + const host = createHostWithPlugin([aTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs], session); + verifyCommandWithMetadata(session, host, { + command: protocol.CommandTypes.CompletionInfo, + arguments: completionRequestArgs + }, { + isGlobalCompletion: false, + isMemberCompletion: true, + isNewIdentifierLocation: false, + entries: expectedCompletionEntries + }); + }); + + it("returns undefined correctly", () => { + const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { const x = 0; } }` }; + const host = createHostWithPlugin([aTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs], session); + verifyCommandWithMetadata(session, host, { + command: protocol.CommandTypes.Completions, + arguments: { file: aTs.path, line: 1, offset: aTs.content.indexOf("x") + 1 } + }, /*expectedResponseBody*/ undefined); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/navTo.ts b/src/testRunner/unittests/tsserver/navTo.ts new file mode 100644 index 00000000000..5d45e537ede --- /dev/null +++ b/src/testRunner/unittests/tsserver/navTo.ts @@ -0,0 +1,30 @@ +namespace ts.projectSystem { + describe("tsserver:: navigate-to for javascript project", () => { + function containsNavToItem(items: protocol.NavtoItem[], itemName: string, itemKind: string) { + return find(items, item => item.name === itemName && item.kind === itemKind) !== undefined; + } + + it("should not include type symbols", () => { + const file1: File = { + path: "/a/b/file1.js", + content: "function foo() {}" + }; + const configFile: File = { + path: "/a/b/jsconfig.json", + content: "{}" + }; + const host = createServerHost([file1, configFile, libFile]); + const session = createSession(host); + openFilesForSession([file1], session); + + // 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 = 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 = session.executeCommand(localFunctionNavToRequst).response as protocol.NavtoItem[]; + assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/occurences.ts b/src/testRunner/unittests/tsserver/occurences.ts new file mode 100644 index 00000000000..bcdadd3872c --- /dev/null +++ b/src/testRunner/unittests/tsserver/occurences.ts @@ -0,0 +1,47 @@ +namespace ts.projectSystem { + describe("tsserver:: occurence highlight on string", () => { + it("should be marked if only on string values", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: `let t1 = "div";\nlet t2 = "div";\nlet t3 = { "div": 123 };\nlet t4 = t3["div"];` + }; + + const host = createServerHost([file1]); + const session = createSession(host); + const projectService = session.getProjectService(); + + projectService.openClientFile(file1.path); + { + const highlightRequest = makeSessionRequest( + CommandNames.Occurrences, + { file: file1.path, line: 1, offset: 11 } + ); + const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]; + const firstOccurence = highlightResponse[0]; + assert.isTrue(firstOccurence.isInString, "Highlights should be marked with isInString"); + } + + { + const highlightRequest = makeSessionRequest( + CommandNames.Occurrences, + { file: file1.path, line: 3, offset: 13 } + ); + const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]; + assert.isTrue(highlightResponse.length === 2); + const firstOccurence = highlightResponse[0]; + assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on property name"); + } + + { + const highlightRequest = makeSessionRequest( + CommandNames.Occurrences, + { file: file1.path, line: 4, offset: 14 } + ); + const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]; + assert.isTrue(highlightResponse.length === 2); + const firstOccurence = highlightResponse[0]; + assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on indexer"); + } + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/openFile.ts b/src/testRunner/unittests/tsserver/openFile.ts new file mode 100644 index 00000000000..187b5043fcc --- /dev/null +++ b/src/testRunner/unittests/tsserver/openFile.ts @@ -0,0 +1,108 @@ +namespace ts.projectSystem { + describe("tsserver:: Open-file", () => { + it("can be reloaded with empty content", () => { + const f = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const projectFileName = "externalProject"; + const host = createServerHost([f]); + const projectService = createProjectService(host); + // create a project + projectService.openExternalProject({ projectFileName, rootFiles: [toExternalFile(f.path)], options: {} }); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + + const p = projectService.externalProjects[0]; + // force to load the content of the file + p.updateGraph(); + + const scriptInfo = p.getScriptInfo(f.path)!; + checkSnapLength(scriptInfo.getSnapshot(), f.content.length); + + // open project and replace its content with empty string + projectService.openClientFile(f.path, ""); + checkSnapLength(scriptInfo.getSnapshot(), 0); + }); + function checkSnapLength(snap: IScriptSnapshot, expectedLength: number) { + assert.equal(snap.getLength(), expectedLength, "Incorrect snapshot size"); + } + + function verifyOpenFileWorks(useCaseSensitiveFileNames: boolean) { + const file1: File = { + path: "/a/b/src/app.ts", + content: "let x = 10;" + }; + const file2: File = { + path: "/a/B/lib/module2.ts", + content: "let z = 10;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: "" + }; + const configFile2: File = { + path: "/a/tsconfig.json", + content: "" + }; + const host = createServerHost([file1, file2, configFile, configFile2], { + useCaseSensitiveFileNames + }); + const service = createProjectService(host); + + // Open file1 -> configFile + verifyConfigFileName(file1, "/a", configFile); + verifyConfigFileName(file1, "/a/b", configFile); + verifyConfigFileName(file1, "/a/B", configFile); + + // Open file2 use root "/a/b" + verifyConfigFileName(file2, "/a", useCaseSensitiveFileNames ? configFile2 : configFile); + verifyConfigFileName(file2, "/a/b", useCaseSensitiveFileNames ? configFile2 : configFile); + verifyConfigFileName(file2, "/a/B", useCaseSensitiveFileNames ? undefined : configFile); + + function verifyConfigFileName(file: File, projectRoot: string, expectedConfigFile: File | undefined) { + const { configFileName } = service.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRoot); + assert.equal(configFileName, expectedConfigFile && expectedConfigFile.path); + service.closeClientFile(file.path); + } + } + it("works when project root is used with case-sensitive system", () => { + verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ true); + }); + + it("works when project root is used with case-insensitive system", () => { + verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ false); + }); + + it("uses existing project even if project refresh is pending", () => { + const projectFolder = "/user/someuser/projects/myproject"; + const aFile: File = { + path: `${projectFolder}/src/a.ts`, + content: "export const x = 0;" + }; + const configFile: File = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const files = [aFile, configFile, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(aFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder); + verifyProject(); + + const bFile: File = { + path: `${projectFolder}/src/b.ts`, + content: `export {}; declare module "./a" { export const y: number; }` + }; + files.push(bFile); + host.reloadFS(files); + service.openClientFile(bFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder); + verifyProject(); + + function verifyProject() { + assert.isDefined(service.configuredProjects.get(configFile.path)); + const project = service.configuredProjects.get(configFile.path)!; + checkProjectActualFiles(project, files.map(f => f.path)); + } + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts new file mode 100644 index 00000000000..c7a42d6b813 --- /dev/null +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -0,0 +1,658 @@ +namespace ts.projectSystem { + describe("tsserver:: with project references and tsbuild", () => { + function createHost(files: ReadonlyArray, rootNames: ReadonlyArray) { + const host = createServerHost(files); + + // ts build should succeed + const solutionBuilder = tscWatch.createSolutionBuilder(host, rootNames, {}); + solutionBuilder.buildAllProjects(); + assert.equal(host.getOutput().length, 0); + + return host; + } + + describe("with container project", () => { + function getProjectFiles(project: string): [File, File] { + return [ + TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"), + TestFSWithWatch.getTsBuildProjectFile(project, "index.ts"), + ]; + } + + const project = "container"; + const containerLib = getProjectFiles("container/lib"); + const containerExec = getProjectFiles("container/exec"); + const containerCompositeExec = getProjectFiles("container/compositeExec"); + const containerConfig = TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"); + const files = [libFile, ...containerLib, ...containerExec, ...containerCompositeExec, containerConfig]; + + it("does not error on container only project", () => { + const host = createHost(files, [containerConfig.path]); + + // Open external project for the folder + const session = createSession(host); + const service = session.getProjectService(); + service.openExternalProjects([{ + projectFileName: TestFSWithWatch.getTsBuildProjectFilePath(project, project), + rootFiles: files.map(f => ({ fileName: f.path })), + options: {} + }]); + checkNumberOfProjects(service, { configuredProjects: 4 }); + files.forEach(f => { + const args: protocol.FileRequestArgs = { + file: f.path, + projectFileName: endsWith(f.path, "tsconfig.json") ? f.path : undefined + }; + const syntaxDiagnostics = session.executeCommandSeq({ + command: protocol.CommandTypes.SyntacticDiagnosticsSync, + arguments: args + }).response; + assert.deepEqual(syntaxDiagnostics, []); + const semanticDiagnostics = session.executeCommandSeq({ + command: protocol.CommandTypes.SemanticDiagnosticsSync, + arguments: args + }).response; + assert.deepEqual(semanticDiagnostics, []); + }); + const containerProject = service.configuredProjects.get(containerConfig.path)!; + checkProjectActualFiles(containerProject, [containerConfig.path]); + const optionsDiagnostics = session.executeCommandSeq({ + command: protocol.CommandTypes.CompilerOptionsDiagnosticsFull, + arguments: { projectFileName: containerProject.projectName } + }).response; + assert.deepEqual(optionsDiagnostics, []); + }); + + it("can successfully find references with --out options", () => { + const host = createHost(files, [containerConfig.path]); + const session = createSession(host); + openFilesForSession([containerCompositeExec[1]], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const locationOfMyConst = protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst"); + const response = session.executeCommandSeq({ + command: protocol.CommandTypes.Rename, + arguments: { + file: containerCompositeExec[1].path, + ...locationOfMyConst + } + }).response as protocol.RenameResponseBody; + + + const myConstLen = "myConst".length; + const locationOfMyConstInLib = protocolLocationFromSubstring(containerLib[1].content, "myConst"); + assert.deepEqual(response.locs, [ + { file: containerCompositeExec[1].path, locs: [{ start: locationOfMyConst, end: { line: locationOfMyConst.line, offset: locationOfMyConst.offset + myConstLen } }] }, + { file: containerLib[1].path, locs: [{ start: locationOfMyConstInLib, end: { line: locationOfMyConstInLib.line, offset: locationOfMyConstInLib.offset + myConstLen } }] } + ]); + }); + }); + + describe("with main and depedency project", () => { + const projectLocation = "/user/username/projects/myproject"; + const dependecyLocation = `${projectLocation}/dependency`; + const mainLocation = `${projectLocation}/main`; + const dependencyTs: File = { + path: `${dependecyLocation}/FnS.ts`, + content: `export function fn1() { } +export function fn2() { } +export function fn3() { } +export function fn4() { } +export function fn5() { } +` + }; + const dependencyConfig: File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true } }) + }; + + const mainTs: File = { + path: `${mainLocation}/main.ts`, + content: `import { + fn1, + fn2, + fn3, + fn4, + fn5 +} from '../dependency/fns' + +fn1(); +fn2(); +fn3(); +fn4(); +fn5(); +` + }; + const mainConfig: File = { + path: `${mainLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, declarationMap: true }, + references: [{ path: "../dependency" }] + }) + }; + + const randomFile: File = { + path: `${projectLocation}/random/random.ts`, + content: "let a = 10;" + }; + const randomConfig: File = { + path: `${projectLocation}/random/tsconfig.json`, + content: "{}" + }; + const dtsLocation = `${dependecyLocation}/FnS.d.ts`; + const dtsPath = dtsLocation.toLowerCase() as Path; + const dtsMapLocation = `${dtsLocation}.map`; + const dtsMapPath = dtsMapLocation.toLowerCase() as Path; + + const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile, randomFile, randomConfig]; + + function verifyScriptInfos(session: TestSession, host: TestServerHost, openInfos: ReadonlyArray, closedInfos: ReadonlyArray, otherWatchedFiles: ReadonlyArray) { + checkScriptInfos(session.getProjectService(), openInfos.concat(closedInfos)); + checkWatchedFiles(host, closedInfos.concat(otherWatchedFiles).map(f => f.toLowerCase())); + } + + function verifyInfosWithRandom(session: TestSession, host: TestServerHost, openInfos: ReadonlyArray, closedInfos: ReadonlyArray, otherWatchedFiles: ReadonlyArray) { + verifyScriptInfos(session, host, openInfos.concat(randomFile.path), closedInfos, otherWatchedFiles.concat(randomConfig.path)); + } + + function verifyOnlyRandomInfos(session: TestSession, host: TestServerHost) { + verifyScriptInfos(session, host, [randomFile.path], [libFile.path], [randomConfig.path]); + } + + // Returns request and expected Response, expected response when no map file + interface SessionAction { + reqName: string; + request: Partial; + expectedResponse: Response; + expectedResponseNoMap?: Response; + expectedResponseNoDts?: Response; + } + function gotoDefintinionFromMainTs(fn: number): SessionAction { + const textSpan = usageSpan(fn); + const definition: protocol.FileSpan = { file: dependencyTs.path, ...definitionSpan(fn) }; + const declareSpaceLength = "declare ".length; + return { + reqName: "goToDef", + request: { + command: protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: { file: mainTs.path, ...textSpan.start } + }, + expectedResponse: { + // To dependency + definitions: [definition], + textSpan + }, + expectedResponseNoMap: { + // To the dts + definitions: [{ file: dtsPath, start: { line: fn, offset: definition.start.offset + declareSpaceLength }, end: { line: fn, offset: definition.end.offset + declareSpaceLength } }], + textSpan + }, + expectedResponseNoDts: { + // To import declaration + definitions: [{ file: mainTs.path, ...importSpan(fn) }], + textSpan + } + }; + } + + function definitionSpan(fn: number): protocol.TextSpan { + return { start: { line: fn, offset: 17 }, end: { line: fn, offset: 20 } }; + } + function importSpan(fn: number): protocol.TextSpan { + return { start: { line: fn + 1, offset: 5 }, end: { line: fn + 1, offset: 8 } }; + } + function usageSpan(fn: number): protocol.TextSpan { + return { start: { line: fn + 8, offset: 1 }, end: { line: fn + 8, offset: 4 } }; + } + + function renameFromDependencyTs(fn: number): SessionAction { + const triggerSpan = definitionSpan(fn); + return { + reqName: "rename", + request: { + command: protocol.CommandTypes.Rename, + arguments: { file: dependencyTs.path, ...triggerSpan.start } + }, + expectedResponse: { + info: { + canRename: true, + fileToRename: undefined, + displayName: `fn${fn}`, + fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`, + kind: ScriptElementKind.functionElement, + kindModifiers: "export", + triggerSpan + }, + locs: [ + { file: dependencyTs.path, locs: [triggerSpan] } + ] + } + }; + } + + function renameFromDependencyTsWithBothProjectsOpen(fn: number): SessionAction { + const { reqName, request, expectedResponse } = renameFromDependencyTs(fn); + const { info, locs } = expectedResponse; + return { + reqName, + request, + expectedResponse: { + info, + locs: [ + locs[0], + { + file: mainTs.path, + locs: [ + importSpan(fn), + usageSpan(fn) + ] + } + ] + }, + // Only dependency result + expectedResponseNoMap: expectedResponse, + expectedResponseNoDts: expectedResponse + }; + } + + // Returns request and expected Response + type SessionActionGetter = (fn: number) => SessionAction; + // Open File, expectedProjectActualFiles, actionGetter, openFileLastLine + interface DocumentPositionMapperVerifier { + openFile: File; + expectedProjectActualFiles: ReadonlyArray; + actionGetter: SessionActionGetter; + openFileLastLine: number; + } + function verifyDocumentPositionMapperUpdates( + mainScenario: string, + verifier: ReadonlyArray, + closedInfos: ReadonlyArray) { + + const openFiles = verifier.map(v => v.openFile); + const expectedProjectActualFiles = verifier.map(v => v.expectedProjectActualFiles); + const actionGetters = verifier.map(v => v.actionGetter); + const openFileLastLines = verifier.map(v => v.openFileLastLine); + + const configFiles = openFiles.map(openFile => `${getDirectoryPath(openFile.path)}/tsconfig.json`); + const openInfos = openFiles.map(f => f.path); + // When usage and dependency are used, dependency config is part of closedInfo so ignore + const otherWatchedFiles = verifier.length > 1 ? [configFiles[0]] : configFiles; + function openTsFile(onHostCreate?: (host: TestServerHost) => void) { + const host = createHost(files, [mainConfig.path]); + if (onHostCreate) { + onHostCreate(host); + } + const session = createSession(host); + openFilesForSession([...openFiles, randomFile], session); + return { host, session }; + } + + function checkProject(session: TestSession, noDts?: true) { + const service = session.getProjectService(); + checkNumberOfProjects(service, { configuredProjects: 1 + verifier.length }); + configFiles.forEach((configFile, index) => { + checkProjectActualFiles( + service.configuredProjects.get(configFile)!, + noDts ? + expectedProjectActualFiles[index].filter(f => f.toLowerCase() !== dtsPath) : + expectedProjectActualFiles[index] + ); + }); + } + + function verifyInfos(session: TestSession, host: TestServerHost) { + verifyInfosWithRandom(session, host, openInfos, closedInfos, otherWatchedFiles); + } + + function verifyInfosWhenNoMapFile(session: TestSession, host: TestServerHost, dependencyTsOK?: true) { + const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined); + verifyInfosWithRandom( + session, + host, + openInfos, + closedInfos.filter(f => f !== dtsMapClosedInfo && (dependencyTsOK || f !== dependencyTs.path)), + dtsMapClosedInfo ? otherWatchedFiles.concat(dtsMapClosedInfo) : otherWatchedFiles + ); + } + + function verifyInfosWhenNoDtsFile(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) { + const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined); + const dtsClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsPath ? f : undefined); + verifyInfosWithRandom( + session, + host, + openInfos, + closedInfos.filter(f => (dependencyTsAndMapOk || f !== dtsMapClosedInfo) && f !== dtsClosedInfo && (dependencyTsAndMapOk || f !== dependencyTs.path)), + // When project actual file contains dts, it needs to be watched + dtsClosedInfo && expectedProjectActualFiles.some(expectedProjectActualFiles => expectedProjectActualFiles.some(f => f.toLowerCase() === dtsPath)) ? + otherWatchedFiles.concat(dtsClosedInfo) : + otherWatchedFiles + ); + } + + function verifyDocumentPositionMapper(session: TestSession, dependencyMap: server.ScriptInfo, documentPositionMapper: server.ScriptInfo["documentPositionMapper"], notEqual?: true) { + assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap); + if (notEqual) { + assert.notStrictEqual(dependencyMap.documentPositionMapper, documentPositionMapper); + } + else { + assert.strictEqual(dependencyMap.documentPositionMapper, documentPositionMapper); + } + } + + function action(actionGetter: SessionActionGetter, fn: number, session: TestSession) { + const { reqName, request, expectedResponse, expectedResponseNoMap, expectedResponseNoDts } = actionGetter(fn); + const { response } = session.executeCommandSeq(request); + return { reqName, response, expectedResponse, expectedResponseNoMap, expectedResponseNoDts }; + } + + function firstAction(session: TestSession) { + actionGetters.forEach(actionGetter => action(actionGetter, 1, session)); + } + + function verifyAllFnActionWorker(session: TestSession, verifyAction: (result: ReturnType, dtsInfo: server.ScriptInfo | undefined, isFirst: boolean) => void, dtsAbsent?: true) { + // action + let isFirst = true; + for (const actionGetter of actionGetters) { + for (let fn = 1; fn <= 5; fn++) { + const result = action(actionGetter, fn, session); + const dtsInfo = session.getProjectService().filenameToScriptInfo.get(dtsPath); + if (dtsAbsent) { + assert.isUndefined(dtsInfo); + } + else { + assert.isDefined(dtsInfo); + } + verifyAction(result, dtsInfo, isFirst); + isFirst = false; + } + } + } + + function verifyAllFnAction( + session: TestSession, + host: TestServerHost, + firstDocumentPositionMapperNotEquals?: true, + dependencyMap?: server.ScriptInfo, + documentPositionMapper?: server.ScriptInfo["documentPositionMapper"] + ) { + // action + verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse }, dtsInfo, isFirst) => { + assert.deepEqual(response, expectedResponse, `Failed on ${reqName}`); + verifyInfos(session, host); + assert.equal(dtsInfo!.sourceMapFilePath, dtsMapPath); + if (isFirst) { + if (dependencyMap) { + verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper, firstDocumentPositionMapperNotEquals); + documentPositionMapper = dependencyMap.documentPositionMapper; + } + else { + dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!; + documentPositionMapper = dependencyMap.documentPositionMapper; + } + } + else { + verifyDocumentPositionMapper(session, dependencyMap!, documentPositionMapper); + } + }); + return { dependencyMap: dependencyMap!, documentPositionMapper }; + } + + function verifyAllFnActionWithNoMap( + session: TestSession, + host: TestServerHost, + dependencyTsOK?: true + ) { + let sourceMapFilePath: server.ScriptInfo["sourceMapFilePath"]; + // action + verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoMap }, dtsInfo, isFirst) => { + assert.deepEqual(response, expectedResponseNoMap || expectedResponse, `Failed on ${reqName}`); + verifyInfosWhenNoMapFile(session, host, dependencyTsOK); + assert.isUndefined(session.getProjectService().filenameToScriptInfo.get(dtsMapPath)); + if (isFirst) { + assert.isNotString(dtsInfo!.sourceMapFilePath); + assert.isNotFalse(dtsInfo!.sourceMapFilePath); + assert.isDefined(dtsInfo!.sourceMapFilePath); + sourceMapFilePath = dtsInfo!.sourceMapFilePath; + } + else { + assert.equal(dtsInfo!.sourceMapFilePath, sourceMapFilePath); + } + }); + return sourceMapFilePath; + } + + function verifyAllFnActionWithNoDts( + session: TestSession, + host: TestServerHost, + dependencyTsAndMapOk?: true + ) { + // action + verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoDts }) => { + assert.deepEqual(response, expectedResponseNoDts || expectedResponse, `Failed on ${reqName}`); + verifyInfosWhenNoDtsFile(session, host, dependencyTsAndMapOk); + }, /*dtsAbsent*/ true); + } + + function verifyScenarioWithChangesWorker( + change: (host: TestServerHost, session: TestSession) => void, + afterActionDocumentPositionMapperNotEquals: true | undefined, + timeoutBeforeAction: boolean + ) { + const { host, session } = openTsFile(); + + // Create DocumentPositionMapper + firstAction(session); + const dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!; + const documentPositionMapper = dependencyMap.documentPositionMapper; + + // change + change(host, session); + if (timeoutBeforeAction) { + host.runQueuedTimeoutCallbacks(); + checkProject(session); + verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper); + } + + // action + verifyAllFnAction(session, host, afterActionDocumentPositionMapperNotEquals, dependencyMap, documentPositionMapper); + } + + function verifyScenarioWithChanges( + scenarioName: string, + change: (host: TestServerHost, session: TestSession) => void, + afterActionDocumentPositionMapperNotEquals?: true + ) { + describe(scenarioName, () => { + it("when timeout occurs before request", () => { + verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ true); + }); + + it("when timeout does not occur before request", () => { + verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ false); + }); + }); + } + + function verifyMainScenarioAndScriptInfoCollection(session: TestSession, host: TestServerHost) { + // Main scenario action + const { dependencyMap, documentPositionMapper } = verifyAllFnAction(session, host); + checkProject(session); + verifyInfos(session, host); + + // Collecting at this point retains dependency.d.ts and map + closeFilesForSession([randomFile], session); + openFilesForSession([randomFile], session); + verifyInfos(session, host); + verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper); + + // Closing open file, removes dependencies too + closeFilesForSession([...openFiles, randomFile], session); + openFilesForSession([randomFile], session); + verifyOnlyRandomInfos(session, host); + } + + function verifyMainScenarioAndScriptInfoCollectionWithNoMap(session: TestSession, host: TestServerHost, dependencyTsOKInScenario?: true) { + // Main scenario action + verifyAllFnActionWithNoMap(session, host, dependencyTsOKInScenario); + + // Collecting at this point retains dependency.d.ts and map watcher + closeFilesForSession([randomFile], session); + openFilesForSession([randomFile], session); + verifyInfosWhenNoMapFile(session, host); + + // Closing open file, removes dependencies too + closeFilesForSession([...openFiles, randomFile], session); + openFilesForSession([randomFile], session); + verifyOnlyRandomInfos(session, host); + } + + function verifyMainScenarioAndScriptInfoCollectionWithNoDts(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) { + // Main scenario action + verifyAllFnActionWithNoDts(session, host, dependencyTsAndMapOk); + + // Collecting at this point retains dependency.d.ts and map watcher + closeFilesForSession([randomFile], session); + openFilesForSession([randomFile], session); + verifyInfosWhenNoDtsFile(session, host); + + // Closing open file, removes dependencies too + closeFilesForSession([...openFiles, randomFile], session); + openFilesForSession([randomFile], session); + verifyOnlyRandomInfos(session, host); + } + + function verifyScenarioWhenFileNotPresent( + scenarioName: string, + fileLocation: string, + verifyScenarioAndScriptInfoCollection: (session: TestSession, host: TestServerHost, dependencyTsOk?: true) => void, + noDts?: true + ) { + describe(scenarioName, () => { + it(mainScenario, () => { + const { host, session } = openTsFile(host => host.deleteFile(fileLocation)); + checkProject(session, noDts); + + verifyScenarioAndScriptInfoCollection(session, host); + }); + + it("when file is created", () => { + let fileContents: string | undefined; + const { host, session } = openTsFile(host => { + fileContents = host.readFile(fileLocation); + host.deleteFile(fileLocation); + }); + firstAction(session); + + host.writeFile(fileLocation, fileContents!); + verifyMainScenarioAndScriptInfoCollection(session, host); + }); + + it("when file is deleted", () => { + const { host, session } = openTsFile(); + firstAction(session); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(fileLocation); + verifyScenarioAndScriptInfoCollection(session, host, /*dependencyTsOk*/ true); + }); + }); + } + + it(mainScenario, () => { + const { host, session } = openTsFile(); + checkProject(session); + + verifyMainScenarioAndScriptInfoCollection(session, host); + }); + + // Edit + verifyScenarioWithChanges( + "when usage file changes, document position mapper doesnt change", + (_host, session) => openFiles.forEach( + (openFile, index) => session.executeCommandSeq({ + command: protocol.CommandTypes.Change, + arguments: { file: openFile.path, line: openFileLastLines[index], offset: 1, endLine: openFileLastLines[index], endOffset: 1, insertString: "const x = 10;" } + }) + ) + ); + + // Edit dts to add new fn + verifyScenarioWithChanges( + "when dependency .d.ts changes, document position mapper doesnt change", + host => host.writeFile( + dtsLocation, + host.readFile(dtsLocation)!.replace( + "//# sourceMappingURL=FnS.d.ts.map", + `export declare function fn6(): void; +//# sourceMappingURL=FnS.d.ts.map` + ) + ) + ); + + // Edit map file to represent added new line + verifyScenarioWithChanges( + "when dependency file's map changes", + host => host.writeFile( + dtsMapLocation, + `{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}` + ), + /*afterActionDocumentPositionMapperNotEquals*/ true + ); + + verifyScenarioWhenFileNotPresent( + "when map file is not present", + dtsMapLocation, + verifyMainScenarioAndScriptInfoCollectionWithNoMap + ); + + verifyScenarioWhenFileNotPresent( + "when .d.ts file is not present", + dtsLocation, + verifyMainScenarioAndScriptInfoCollectionWithNoDts, + /*noDts*/ true + ); + } + + const usageVerifier: DocumentPositionMapperVerifier = { + openFile: mainTs, + expectedProjectActualFiles: [mainTs.path, libFile.path, mainConfig.path, dtsPath], + actionGetter: gotoDefintinionFromMainTs, + openFileLastLine: 14 + }; + describe("from project that uses dependency", () => { + const closedInfos = [dependencyTs.path, dependencyConfig.path, libFile.path, dtsPath, dtsMapLocation]; + verifyDocumentPositionMapperUpdates( + "can go to definition correctly", + [usageVerifier], + closedInfos + ); + }); + + const definingVerifier: DocumentPositionMapperVerifier = { + openFile: dependencyTs, + expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path], + actionGetter: renameFromDependencyTs, + openFileLastLine: 6 + }; + describe("from defining project", () => { + const closedInfos = [libFile.path, dtsLocation, dtsMapLocation]; + verifyDocumentPositionMapperUpdates( + "rename locations from dependency", + [definingVerifier], + closedInfos + ); + }); + + describe("when opening depedency and usage project", () => { + const closedInfos = [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path]; + verifyDocumentPositionMapperUpdates( + "goto Definition in usage and rename locations from defining project", + [usageVerifier, { ...definingVerifier, actionGetter: renameFromDependencyTsWithBothProjectsOpen }], + closedInfos + ); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/refactors.ts b/src/testRunner/unittests/tsserver/refactors.ts new file mode 100644 index 00000000000..2e916f50f9b --- /dev/null +++ b/src/testRunner/unittests/tsserver/refactors.ts @@ -0,0 +1,120 @@ +namespace ts.projectSystem { + describe("tsserver:: refactors", () => { + it("use formatting options", () => { + const file = { + path: "/a.ts", + content: "function f() {\n 1;\n}", + }; + const host = createServerHost([file]); + const session = createSession(host); + openFilesForSession([file], session); + + const response0 = session.executeCommandSeq({ + command: server.protocol.CommandTypes.Configure, + arguments: { + formatOptions: { + indentSize: 2, + }, + }, + }).response; + assert.deepEqual(response0, /*expected*/ undefined); + + const response1 = session.executeCommandSeq({ + command: server.protocol.CommandTypes.GetEditsForRefactor, + arguments: { + refactor: "Extract Symbol", + action: "function_scope_1", + file: "/a.ts", + startLine: 2, + startOffset: 3, + endLine: 2, + endOffset: 4, + }, + }).response; + assert.deepEqual(response1, { + edits: [ + { + fileName: "/a.ts", + textChanges: [ + { + start: { line: 2, offset: 3 }, + end: { line: 2, offset: 5 }, + newText: "newFunction();", + }, + { + start: { line: 3, offset: 2 }, + end: { line: 3, offset: 2 }, + newText: "\n\nfunction newFunction() {\n 1;\n}\n", + }, + ] + } + ], + renameFilename: "/a.ts", + renameLocation: { line: 2, offset: 3 }, + }); + }); + + it("handles text changes in tsconfig.json", () => { + const aTs = { + path: "/a.ts", + content: "export const a = 0;", + }; + const tsconfig = { + path: "/tsconfig.json", + content: '{ "files": ["./a.ts"] }', + }; + + const session = createSession(createServerHost([aTs, tsconfig])); + openFilesForSession([aTs], session); + + const response1 = session.executeCommandSeq({ + command: server.protocol.CommandTypes.GetEditsForRefactor, + arguments: { + refactor: "Move to a new file", + action: "Move to a new file", + file: "/a.ts", + startLine: 1, + startOffset: 1, + endLine: 1, + endOffset: 20, + }, + }).response; + assert.deepEqual(response1, { + edits: [ + { + fileName: "/a.ts", + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 20 }, + newText: "", + }, + ], + }, + { + fileName: "/tsconfig.json", + textChanges: [ + { + start: { line: 1, offset: 21 }, + end: { line: 1, offset: 21 }, + newText: ", \"./a.1.ts\"", + }, + ], + }, + { + fileName: "/a.1.ts", + textChanges: [ + { + start: { line: 0, offset: 0 }, + end: { line: 0, offset: 0 }, + newText: "export const a = 0;\n", + }, + ], + } + ], + renameFilename: undefined, + renameLocation: undefined, + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/rename.ts b/src/testRunner/unittests/tsserver/rename.ts new file mode 100644 index 00000000000..3b4d77263e6 --- /dev/null +++ b/src/testRunner/unittests/tsserver/rename.ts @@ -0,0 +1,53 @@ +namespace ts.projectSystem { + describe("tsserver:: rename", () => { + it("works with fileToRename", () => { + const aTs: File = { path: "/a.ts", content: "export const a = 0;" }; + const bTs: File = { path: "/b.ts", content: 'import { a } from "./a";' }; + + const session = createSession(createServerHost([aTs, bTs])); + openFilesForSession([bTs], session); + + const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(bTs, 'a";')); + assert.deepEqual(response, { + info: { + canRename: true, + fileToRename: aTs.path, + displayName: aTs.path, + fullDisplayName: aTs.path, + kind: ScriptElementKind.moduleElement, + kindModifiers: "", + triggerSpan: protocolTextSpanFromSubstring(bTs.content, "a", { index: 1 }), + }, + locs: [{ file: bTs.path, locs: [protocolRenameSpanFromSubstring(bTs.content, "./a")] }], + }); + }); + + it("works with prefixText and suffixText", () => { + const aTs: File = { path: "/a.ts", content: "const x = 0; const o = { x };" }; + const session = createSession(createServerHost([aTs])); + openFilesForSession([aTs], session); + + const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "x")); + assert.deepEqual(response, { + info: { + canRename: true, + fileToRename: undefined, + displayName: "x", + fullDisplayName: "x", + kind: ScriptElementKind.constElement, + kindModifiers: ScriptElementKindModifier.none, + triggerSpan: protocolTextSpanFromSubstring(aTs.content, "x"), + }, + locs: [ + { + file: aTs.path, + locs: [ + protocolRenameSpanFromSubstring(aTs.content, "x"), + protocolRenameSpanFromSubstring(aTs.content, "x", { index: 1 }, { prefixText: "x: " }), + ], + }, + ], + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/resolutionCache.ts b/src/testRunner/unittests/tsserver/resolutionCache.ts index a07cb7721d4..9f75455e226 100644 --- a/src/testRunner/unittests/tsserver/resolutionCache.ts +++ b/src/testRunner/unittests/tsserver/resolutionCache.ts @@ -5,7 +5,7 @@ namespace ts.projectSystem { return resolutionTrace; } - describe("resolutionCache:: tsserverProjectSystem extra resolution pass in lshost", () => { + describe("tsserver:: resolutionCache:: tsserverProjectSystem extra resolution pass in lshost", () => { it("can load typings that are proper modules", () => { const file1 = { path: "/a/b/app.js", @@ -46,7 +46,7 @@ namespace ts.projectSystem { }); }); - describe("resolutionCache:: tsserverProjectSystem watching @types", () => { + describe("tsserver:: resolutionCache:: tsserverProjectSystem watching @types", () => { it("works correctly when typings are added or removed", () => { const f1 = { path: "/a/b/app.ts", @@ -92,7 +92,7 @@ namespace ts.projectSystem { }); }); - describe("resolutionCache:: tsserverProjectSystem add the missing module file for inferred project", () => { + describe("tsserver:: resolutionCache:: tsserverProjectSystem add the missing module file for inferred project", () => { it("should remove the `module not found` error", () => { const moduleFile = { path: "/a/b/moduleFile.ts", @@ -358,7 +358,7 @@ namespace ts.projectSystem { }); }); - describe("resolutionCache:: tsserverProjectSystem rename a module file and rename back", () => { + describe("tsserver:: resolutionCache:: tsserverProjectSystem rename a module file and rename back", () => { it("should restore the states for inferred projects", () => { const moduleFile = { path: "/a/b/moduleFile.ts", @@ -493,7 +493,7 @@ namespace ts.projectSystem { }); }); - describe("resolutionCache:: tsserverProjectSystem module resolution caching", () => { + describe("tsserver:: resolutionCache:: tsserverProjectSystem module resolution caching", () => { const projectLocation = "/user/username/projects/myproject"; const configFile: File = { path: `${projectLocation}/tsconfig.json`, diff --git a/src/testRunner/unittests/tsserver/syntaxOperations.ts b/src/testRunner/unittests/tsserver/syntaxOperations.ts new file mode 100644 index 00000000000..c2ec877137a --- /dev/null +++ b/src/testRunner/unittests/tsserver/syntaxOperations.ts @@ -0,0 +1,98 @@ +namespace ts.projectSystem { + describe("tsserver:: syntax operations", () => { + function navBarFull(session: TestSession, file: File) { + return JSON.stringify(session.executeCommandSeq({ + command: protocol.CommandTypes.NavBarFull, + arguments: { file: file.path } + }).response); + } + + function openFile(session: TestSession, file: File) { + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { file: file.path, fileContent: file.content } + }); + } + + it("works when file is removed and added with different content", () => { + const projectRoot = "/user/username/projects/myproject"; + const app: File = { + path: `${projectRoot}/app.ts`, + content: "console.log('Hello world');" + }; + const unitTest1: File = { + path: `${projectRoot}/unitTest1.ts`, + content: `import assert = require('assert'); + +describe("Test Suite 1", () => { + it("Test A", () => { + assert.ok(true, "This shouldn't fail"); + }); + + it("Test B", () => { + assert.ok(1 === 1, "This shouldn't fail"); + assert.ok(false, "This should fail"); + }); +});` + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const files = [app, libFile, tsconfig]; + const host = createServerHost(files); + const session = createSession(host); + const service = session.getProjectService(); + openFile(session, app); + + checkNumberOfProjects(service, { configuredProjects: 1 }); + const project = service.configuredProjects.get(tsconfig.path)!; + const expectedFilesWithoutUnitTest1 = files.map(f => f.path); + checkProjectActualFiles(project, expectedFilesWithoutUnitTest1); + + host.writeFile(unitTest1.path, unitTest1.content); + host.runQueuedTimeoutCallbacks(); + const expectedFilesWithUnitTest1 = expectedFilesWithoutUnitTest1.concat(unitTest1.path); + checkProjectActualFiles(project, expectedFilesWithUnitTest1); + + openFile(session, unitTest1); + checkProjectActualFiles(project, expectedFilesWithUnitTest1); + + const navBarResultUnitTest1 = navBarFull(session, unitTest1); + host.deleteFile(unitTest1.path); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectActualFiles(project, expectedFilesWithoutUnitTest1); + + session.executeCommandSeq({ + command: protocol.CommandTypes.Close, + arguments: { file: unitTest1.path } + }); + checkProjectActualFiles(project, expectedFilesWithoutUnitTest1); + + const unitTest1WithChangedContent: File = { + path: unitTest1.path, + content: `import assert = require('assert'); + +export function Test1() { + assert.ok(true, "This shouldn't fail"); +}; + +export function Test2() { + assert.ok(1 === 1, "This shouldn't fail"); + assert.ok(false, "This should fail"); +};` + }; + host.writeFile(unitTest1.path, unitTest1WithChangedContent.content); + host.runQueuedTimeoutCallbacks(); + checkProjectActualFiles(project, expectedFilesWithUnitTest1); + + openFile(session, unitTest1WithChangedContent); + checkProjectActualFiles(project, expectedFilesWithUnitTest1); + const sourceFile = project.getLanguageService().getNonBoundSourceFile(unitTest1WithChangedContent.path); + assert.strictEqual(sourceFile.text, unitTest1WithChangedContent.content); + + const navBarResultUnitTest1WithChangedContent = navBarFull(session, unitTest1WithChangedContent); + assert.notStrictEqual(navBarResultUnitTest1WithChangedContent, navBarResultUnitTest1, "With changes in contents of unitTest file, we should see changed naviagation bar item result"); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/telemetry.ts b/src/testRunner/unittests/tsserver/telemetry.ts index f2a7d854124..4f4d8ba8b19 100644 --- a/src/testRunner/unittests/tsserver/telemetry.ts +++ b/src/testRunner/unittests/tsserver/telemetry.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("project telemetry", () => { + describe("tsserver:: project telemetry", () => { it("does nothing for inferred project", () => { const file = makeFile("/a.js"); const et = new TestServerEventManager([file]); diff --git a/src/testRunner/unittests/tsserver/textStorage.ts b/src/testRunner/unittests/tsserver/textStorage.ts index 0bb14aa2d90..157734da931 100644 --- a/src/testRunner/unittests/tsserver/textStorage.ts +++ b/src/testRunner/unittests/tsserver/textStorage.ts @@ -1,5 +1,5 @@ namespace ts.textStorage { - describe("Text storage", () => { + describe("tsserver:: Text storage", () => { const f = { path: "/a/app.ts", content: ` diff --git a/src/testRunner/unittests/tsserver/typeAquisition.ts b/src/testRunner/unittests/tsserver/typeAquisition.ts new file mode 100644 index 00000000000..9f487896e03 --- /dev/null +++ b/src/testRunner/unittests/tsserver/typeAquisition.ts @@ -0,0 +1,52 @@ +namespace ts.projectSystem { + describe("tsserver:: autoDiscovery", () => { + it("does not depend on extension", () => { + const file1 = { + path: "/a/b/app.html", + content: "" + }; + const file2 = { + path: "/a/b/app.d.ts", + content: "" + }; + const host = createServerHost([file1, file2]); + const projectService = createProjectService(host); + projectService.openExternalProject({ + projectFileName: "/a/b/proj.csproj", + rootFiles: [toExternalFile(file2.path), { fileName: file1.path, hasMixedContent: true, scriptKind: ScriptKind.JS }], + options: {} + }); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + const typeAcquisition = projectService.externalProjects[0].getTypeAcquisition(); + assert.isTrue(typeAcquisition.enable, "Typine acquisition should be enabled"); + }); + }); + + describe("tsserver:: prefer typings to js", () => { + it("during second resolution pass", () => { + const typingsCacheLocation = "/a/typings"; + const f1 = { + path: "/a/b/app.js", + content: "var x = require('bar')" + }; + const barjs = { + path: "/a/b/node_modules/bar/index.js", + content: "export let x = 1" + }; + const barTypings = { + path: `${typingsCacheLocation}/node_modules/@types/bar/index.d.ts`, + content: "export let y: number" + }; + const config = { + path: "/a/b/jsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true }, exclude: ["node_modules"] }) + }; + const host = createServerHost([f1, barjs, barTypings, config]); + const projectService = createProjectService(host, { typingsInstaller: new TestTypingsInstaller(typingsCacheLocation, /*throttleLimit*/ 5, host) }); + + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, barTypings.path, config.path]); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/typeReferenceDirectives.ts b/src/testRunner/unittests/tsserver/typeReferenceDirectives.ts new file mode 100644 index 00000000000..8318f402a1a --- /dev/null +++ b/src/testRunner/unittests/tsserver/typeReferenceDirectives.ts @@ -0,0 +1,87 @@ +namespace ts.projectSystem { + describe("tsserver:: typeReferenceDirectives", () => { + it("when typeReferenceDirective contains UpperCasePackage", () => { + const projectLocation = "/user/username/projects/myproject"; + const libProjectLocation = `${projectLocation}/lib`; + const typeLib: File = { + path: `${libProjectLocation}/@types/UpperCasePackage/index.d.ts`, + content: `declare class BrokenTest { + constructor(name: string, width: number, height: number, onSelect: Function); + Name: string; + SelectedFile: string; +}` + }; + const appLib: File = { + path: `${libProjectLocation}/@app/lib/index.d.ts`, + content: `/// +declare class TestLib { + issue: BrokenTest; + constructor(); + test(): void; +}` + }; + const testProjectLocation = `${projectLocation}/test`; + const testFile: File = { + path: `${testProjectLocation}/test.ts`, + content: `class TestClass1 { + + constructor() { + var l = new TestLib(); + + } + + public test2() { + var x = new BrokenTest('',0,0,null); + + } +}` + }; + const testConfig: File = { + path: `${testProjectLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "amd", + typeRoots: ["../lib/@types", "../lib/@app"] + } + }) + }; + + const files = [typeLib, appLib, testFile, testConfig, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(testFile.path); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const project = service.configuredProjects.get(testConfig.path)!; + checkProjectActualFiles(project, files.map(f => f.path)); + host.writeFile(appLib.path, appLib.content.replace("test()", "test2()")); + host.checkTimeoutQueueLengthAndRun(2); + }); + + it("when typeReferenceDirective is relative path and in a sibling folder", () => { + const projectRootPath = "/user/username/projects/browser-addon"; + const projectPath = `${projectRootPath}/background`; + const file: File = { + path: `${projectPath}/a.ts`, + content: "let x = 10;" + }; + const tsconfig: File = { + path: `${projectPath}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + types: [ + "../typedefs/filesystem" + ] + } + }) + }; + const filesystem: File = { + path: `${projectRootPath}/typedefs/filesystem.d.ts`, + content: `interface LocalFileSystem { someProperty: string; }` + }; + const files = [file, tsconfig, filesystem, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(file.path); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/typingsInstaller.ts b/src/testRunner/unittests/tsserver/typingsInstaller.ts index f0cd9549e6c..18117a5f961 100644 --- a/src/testRunner/unittests/tsserver/typingsInstaller.ts +++ b/src/testRunner/unittests/tsserver/typingsInstaller.ts @@ -47,7 +47,7 @@ namespace ts.projectSystem { import typingsName = TI.typingsName; - describe("typingsInstaller:: local module", () => { + describe("tsserver:: typingsInstaller:: local module", () => { it("should not be picked up", () => { const f1 = { path: "/a/app.js", @@ -86,7 +86,7 @@ namespace ts.projectSystem { }); }); - describe("typingsInstaller:: General functionality", () => { + describe("tsserver:: typingsInstaller:: General functionality", () => { it("configured projects (typings installed) 1", () => { const file1 = { path: "/a/b/app.js", @@ -1249,7 +1249,7 @@ namespace ts.projectSystem { }); }); - describe("typingsInstaller:: Validate package name:", () => { + describe("tsserver:: typingsInstaller:: Validate package name:", () => { it("name cannot be too long", () => { let packageName = "a"; for (let i = 0; i < 8; i++) { @@ -1273,7 +1273,7 @@ namespace ts.projectSystem { }); }); - describe("typingsInstaller:: Invalid package names", () => { + describe("tsserver:: typingsInstaller:: Invalid package names", () => { it("should not be installed", () => { const f1 = { path: "/a/b/app.js", @@ -1305,7 +1305,7 @@ namespace ts.projectSystem { }); }); - describe("typingsInstaller:: discover typings", () => { + describe("tsserver:: typingsInstaller:: discover typings", () => { const emptySafeList = emptyMap; it("should use mappings from safe list", () => { @@ -1517,7 +1517,7 @@ namespace ts.projectSystem { }); }); - describe("typingsInstaller:: telemetry events", () => { + describe("tsserver:: typingsInstaller:: telemetry events", () => { it("should be received", () => { const f1 = { path: "/a/app.js", @@ -1567,7 +1567,7 @@ namespace ts.projectSystem { }); }); - describe("typingsInstaller:: progress notifications", () => { + describe("tsserver:: typingsInstaller:: progress notifications", () => { it("should be sent for success", () => { const f1 = { path: "/a/app.js", @@ -1676,7 +1676,7 @@ namespace ts.projectSystem { }); }); - describe("typingsInstaller:: npm installation command", () => { + describe("tsserver:: typingsInstaller:: npm installation command", () => { const npmPath = "npm", tsVersion = "2.9.0-dev.20180410"; const packageNames = ["@types/graphql@ts2.8", "@types/highlight.js@ts2.8", "@types/jest@ts2.8", "@types/mini-css-extract-plugin@ts2.8", "@types/mongoose@ts2.8", "@types/pg@ts2.8", "@types/webpack-bundle-analyzer@ts2.8", "@types/enhanced-resolve@ts2.8", "@types/eslint-plugin-prettier@ts2.8", "@types/friendly-errors-webpack-plugin@ts2.8", "@types/hammerjs@ts2.8", "@types/history@ts2.8", "@types/image-size@ts2.8", "@types/js-cookie@ts2.8", "@types/koa-compress@ts2.8", "@types/less@ts2.8", "@types/material-ui@ts2.8", "@types/mysql@ts2.8", "@types/nodemailer@ts2.8", "@types/prettier@ts2.8", "@types/query-string@ts2.8", "@types/react-places-autocomplete@ts2.8", "@types/react-router@ts2.8", "@types/react-router-config@ts2.8", "@types/react-select@ts2.8", "@types/react-transition-group@ts2.8", "@types/redux-form@ts2.8", "@types/abbrev@ts2.8", "@types/accepts@ts2.8", "@types/acorn@ts2.8", "@types/ansi-regex@ts2.8", "@types/ansi-styles@ts2.8", "@types/anymatch@ts2.8", "@types/apollo-codegen@ts2.8", "@types/are-we-there-yet@ts2.8", "@types/argparse@ts2.8", "@types/arr-union@ts2.8", "@types/array-find-index@ts2.8", "@types/array-uniq@ts2.8", "@types/array-unique@ts2.8", "@types/arrify@ts2.8", "@types/assert-plus@ts2.8", "@types/async@ts2.8", "@types/autoprefixer@ts2.8", "@types/aws4@ts2.8", "@types/babel-code-frame@ts2.8", "@types/babel-generator@ts2.8", "@types/babel-plugin-syntax-jsx@ts2.8", "@types/babel-template@ts2.8", "@types/babel-traverse@ts2.8", "@types/babel-types@ts2.8", "@types/babylon@ts2.8", "@types/base64-js@ts2.8", "@types/basic-auth@ts2.8", "@types/big.js@ts2.8", "@types/bl@ts2.8", "@types/bluebird@ts2.8", "@types/body-parser@ts2.8", "@types/bonjour@ts2.8", "@types/boom@ts2.8", "@types/brace-expansion@ts2.8", "@types/braces@ts2.8", "@types/brorand@ts2.8", "@types/browser-resolve@ts2.8", "@types/bson@ts2.8", "@types/buffer-equal@ts2.8", "@types/builtin-modules@ts2.8", "@types/bytes@ts2.8", "@types/callsites@ts2.8", "@types/camelcase@ts2.8", "@types/camelcase-keys@ts2.8", "@types/caseless@ts2.8", "@types/change-emitter@ts2.8", "@types/check-types@ts2.8", "@types/cheerio@ts2.8", "@types/chokidar@ts2.8", "@types/chownr@ts2.8", "@types/circular-json@ts2.8", "@types/classnames@ts2.8", "@types/clean-css@ts2.8", "@types/clone@ts2.8", "@types/co-body@ts2.8", "@types/color@ts2.8", "@types/color-convert@ts2.8", "@types/color-name@ts2.8", "@types/color-string@ts2.8", "@types/colors@ts2.8", "@types/combined-stream@ts2.8", "@types/common-tags@ts2.8", "@types/component-emitter@ts2.8", "@types/compressible@ts2.8", "@types/compression@ts2.8", "@types/concat-stream@ts2.8", "@types/connect-history-api-fallback@ts2.8", "@types/content-disposition@ts2.8", "@types/content-type@ts2.8", "@types/convert-source-map@ts2.8", "@types/cookie@ts2.8", "@types/cookie-signature@ts2.8", "@types/cookies@ts2.8", "@types/core-js@ts2.8", "@types/cosmiconfig@ts2.8", "@types/create-react-class@ts2.8", "@types/cross-spawn@ts2.8", "@types/cryptiles@ts2.8", "@types/css-modules-require-hook@ts2.8", "@types/dargs@ts2.8", "@types/dateformat@ts2.8", "@types/debug@ts2.8", "@types/decamelize@ts2.8", "@types/decompress@ts2.8", "@types/decompress-response@ts2.8", "@types/deep-equal@ts2.8", "@types/deep-extend@ts2.8", "@types/deepmerge@ts2.8", "@types/defined@ts2.8", "@types/del@ts2.8", "@types/depd@ts2.8", "@types/destroy@ts2.8", "@types/detect-indent@ts2.8", "@types/detect-newline@ts2.8", "@types/diff@ts2.8", "@types/doctrine@ts2.8", "@types/download@ts2.8", "@types/draft-js@ts2.8", "@types/duplexer2@ts2.8", "@types/duplexer3@ts2.8", "@types/duplexify@ts2.8", "@types/ejs@ts2.8", "@types/end-of-stream@ts2.8", "@types/entities@ts2.8", "@types/escape-html@ts2.8", "@types/escape-string-regexp@ts2.8", "@types/escodegen@ts2.8", "@types/eslint-scope@ts2.8", "@types/eslint-visitor-keys@ts2.8", "@types/esprima@ts2.8", "@types/estraverse@ts2.8", "@types/etag@ts2.8", "@types/events@ts2.8", "@types/execa@ts2.8", "@types/exenv@ts2.8", "@types/exit@ts2.8", "@types/exit-hook@ts2.8", "@types/expect@ts2.8", "@types/express@ts2.8", "@types/express-graphql@ts2.8", "@types/extend@ts2.8", "@types/extract-zip@ts2.8", "@types/fancy-log@ts2.8", "@types/fast-diff@ts2.8", "@types/fast-levenshtein@ts2.8", "@types/figures@ts2.8", "@types/file-type@ts2.8", "@types/filenamify@ts2.8", "@types/filesize@ts2.8", "@types/finalhandler@ts2.8", "@types/find-root@ts2.8", "@types/find-up@ts2.8", "@types/findup-sync@ts2.8", "@types/forever-agent@ts2.8", "@types/form-data@ts2.8", "@types/forwarded@ts2.8", "@types/fresh@ts2.8", "@types/from2@ts2.8", "@types/fs-extra@ts2.8", "@types/get-caller-file@ts2.8", "@types/get-stdin@ts2.8", "@types/get-stream@ts2.8", "@types/get-value@ts2.8", "@types/glob-base@ts2.8", "@types/glob-parent@ts2.8", "@types/glob-stream@ts2.8", "@types/globby@ts2.8", "@types/globule@ts2.8", "@types/got@ts2.8", "@types/graceful-fs@ts2.8", "@types/gulp-rename@ts2.8", "@types/gulp-sourcemaps@ts2.8", "@types/gulp-util@ts2.8", "@types/gzip-size@ts2.8", "@types/handlebars@ts2.8", "@types/has-ansi@ts2.8", "@types/hasha@ts2.8", "@types/he@ts2.8", "@types/hoek@ts2.8", "@types/html-entities@ts2.8", "@types/html-minifier@ts2.8", "@types/htmlparser2@ts2.8", "@types/http-assert@ts2.8", "@types/http-errors@ts2.8", "@types/http-proxy@ts2.8", "@types/http-proxy-middleware@ts2.8", "@types/indent-string@ts2.8", "@types/inflected@ts2.8", "@types/inherits@ts2.8", "@types/ini@ts2.8", "@types/inline-style-prefixer@ts2.8", "@types/inquirer@ts2.8", "@types/internal-ip@ts2.8", "@types/into-stream@ts2.8", "@types/invariant@ts2.8", "@types/ip@ts2.8", "@types/ip-regex@ts2.8", "@types/is-absolute-url@ts2.8", "@types/is-binary-path@ts2.8", "@types/is-finite@ts2.8", "@types/is-glob@ts2.8", "@types/is-my-json-valid@ts2.8", "@types/is-number@ts2.8", "@types/is-object@ts2.8", "@types/is-path-cwd@ts2.8", "@types/is-path-in-cwd@ts2.8", "@types/is-promise@ts2.8", "@types/is-scoped@ts2.8", "@types/is-stream@ts2.8", "@types/is-svg@ts2.8", "@types/is-url@ts2.8", "@types/is-windows@ts2.8", "@types/istanbul-lib-coverage@ts2.8", "@types/istanbul-lib-hook@ts2.8", "@types/istanbul-lib-instrument@ts2.8", "@types/istanbul-lib-report@ts2.8", "@types/istanbul-lib-source-maps@ts2.8", "@types/istanbul-reports@ts2.8", "@types/jest-diff@ts2.8", "@types/jest-docblock@ts2.8", "@types/jest-get-type@ts2.8", "@types/jest-matcher-utils@ts2.8", "@types/jest-validate@ts2.8", "@types/jpeg-js@ts2.8", "@types/js-base64@ts2.8", "@types/js-string-escape@ts2.8", "@types/js-yaml@ts2.8", "@types/jsbn@ts2.8", "@types/jsdom@ts2.8", "@types/jsesc@ts2.8", "@types/json-parse-better-errors@ts2.8", "@types/json-schema@ts2.8", "@types/json-stable-stringify@ts2.8", "@types/json-stringify-safe@ts2.8", "@types/json5@ts2.8", "@types/jsonfile@ts2.8", "@types/jsontoxml@ts2.8", "@types/jss@ts2.8", "@types/keygrip@ts2.8", "@types/keymirror@ts2.8", "@types/keyv@ts2.8", "@types/klaw@ts2.8", "@types/koa-send@ts2.8", "@types/leven@ts2.8", "@types/listr@ts2.8", "@types/load-json-file@ts2.8", "@types/loader-runner@ts2.8", "@types/loader-utils@ts2.8", "@types/locate-path@ts2.8", "@types/lodash-es@ts2.8", "@types/lodash.assign@ts2.8", "@types/lodash.camelcase@ts2.8", "@types/lodash.clonedeep@ts2.8", "@types/lodash.debounce@ts2.8", "@types/lodash.escape@ts2.8", "@types/lodash.flowright@ts2.8", "@types/lodash.get@ts2.8", "@types/lodash.isarguments@ts2.8", "@types/lodash.isarray@ts2.8", "@types/lodash.isequal@ts2.8", "@types/lodash.isobject@ts2.8", "@types/lodash.isstring@ts2.8", "@types/lodash.keys@ts2.8", "@types/lodash.memoize@ts2.8", "@types/lodash.merge@ts2.8", "@types/lodash.mergewith@ts2.8", "@types/lodash.pick@ts2.8", "@types/lodash.sortby@ts2.8", "@types/lodash.tail@ts2.8", "@types/lodash.template@ts2.8", "@types/lodash.throttle@ts2.8", "@types/lodash.unescape@ts2.8", "@types/lodash.uniq@ts2.8", "@types/log-symbols@ts2.8", "@types/log-update@ts2.8", "@types/loglevel@ts2.8", "@types/loud-rejection@ts2.8", "@types/lru-cache@ts2.8", "@types/make-dir@ts2.8", "@types/map-obj@ts2.8", "@types/media-typer@ts2.8", "@types/mem@ts2.8", "@types/mem-fs@ts2.8", "@types/memory-fs@ts2.8", "@types/meow@ts2.8", "@types/merge-descriptors@ts2.8", "@types/merge-stream@ts2.8", "@types/methods@ts2.8", "@types/micromatch@ts2.8", "@types/mime@ts2.8", "@types/mime-db@ts2.8", "@types/mime-types@ts2.8", "@types/minimatch@ts2.8", "@types/minimist@ts2.8", "@types/minipass@ts2.8", "@types/mkdirp@ts2.8", "@types/mongodb@ts2.8", "@types/morgan@ts2.8", "@types/move-concurrently@ts2.8", "@types/ms@ts2.8", "@types/msgpack-lite@ts2.8", "@types/multimatch@ts2.8", "@types/mz@ts2.8", "@types/negotiator@ts2.8", "@types/node-dir@ts2.8", "@types/node-fetch@ts2.8", "@types/node-forge@ts2.8", "@types/node-int64@ts2.8", "@types/node-ipc@ts2.8", "@types/node-notifier@ts2.8", "@types/nomnom@ts2.8", "@types/nopt@ts2.8", "@types/normalize-package-data@ts2.8", "@types/normalize-url@ts2.8", "@types/number-is-nan@ts2.8", "@types/object-assign@ts2.8", "@types/on-finished@ts2.8", "@types/on-headers@ts2.8", "@types/once@ts2.8", "@types/onetime@ts2.8", "@types/opener@ts2.8", "@types/opn@ts2.8", "@types/optimist@ts2.8", "@types/ora@ts2.8", "@types/os-homedir@ts2.8", "@types/os-locale@ts2.8", "@types/os-tmpdir@ts2.8", "@types/p-cancelable@ts2.8", "@types/p-each-series@ts2.8", "@types/p-event@ts2.8", "@types/p-lazy@ts2.8", "@types/p-limit@ts2.8", "@types/p-locate@ts2.8", "@types/p-map@ts2.8", "@types/p-map-series@ts2.8", "@types/p-reduce@ts2.8", "@types/p-timeout@ts2.8", "@types/p-try@ts2.8", "@types/pako@ts2.8", "@types/parse-glob@ts2.8", "@types/parse-json@ts2.8", "@types/parseurl@ts2.8", "@types/path-exists@ts2.8", "@types/path-is-absolute@ts2.8", "@types/path-parse@ts2.8", "@types/pg-pool@ts2.8", "@types/pg-types@ts2.8", "@types/pify@ts2.8", "@types/pixelmatch@ts2.8", "@types/pkg-dir@ts2.8", "@types/pluralize@ts2.8", "@types/pngjs@ts2.8", "@types/prelude-ls@ts2.8", "@types/pretty-bytes@ts2.8", "@types/pretty-format@ts2.8", "@types/progress@ts2.8", "@types/promise-retry@ts2.8", "@types/proxy-addr@ts2.8", "@types/pump@ts2.8", "@types/q@ts2.8", "@types/qs@ts2.8", "@types/range-parser@ts2.8", "@types/rc@ts2.8", "@types/rc-select@ts2.8", "@types/rc-slider@ts2.8", "@types/rc-tooltip@ts2.8", "@types/rc-tree@ts2.8", "@types/react-event-listener@ts2.8", "@types/react-side-effect@ts2.8", "@types/react-slick@ts2.8", "@types/read-chunk@ts2.8", "@types/read-pkg@ts2.8", "@types/read-pkg-up@ts2.8", "@types/recompose@ts2.8", "@types/recursive-readdir@ts2.8", "@types/relateurl@ts2.8", "@types/replace-ext@ts2.8", "@types/request@ts2.8", "@types/request-promise-native@ts2.8", "@types/require-directory@ts2.8", "@types/require-from-string@ts2.8", "@types/require-relative@ts2.8", "@types/resolve@ts2.8", "@types/resolve-from@ts2.8", "@types/retry@ts2.8", "@types/rx@ts2.8", "@types/rx-lite@ts2.8", "@types/rx-lite-aggregates@ts2.8", "@types/safe-regex@ts2.8", "@types/sane@ts2.8", "@types/sass-graph@ts2.8", "@types/sax@ts2.8", "@types/scriptjs@ts2.8", "@types/semver@ts2.8", "@types/send@ts2.8", "@types/serialize-javascript@ts2.8", "@types/serve-index@ts2.8", "@types/serve-static@ts2.8", "@types/set-value@ts2.8", "@types/shallowequal@ts2.8", "@types/shelljs@ts2.8", "@types/sockjs@ts2.8", "@types/sockjs-client@ts2.8", "@types/source-list-map@ts2.8", "@types/source-map-support@ts2.8", "@types/spdx-correct@ts2.8", "@types/spdy@ts2.8", "@types/split@ts2.8", "@types/sprintf@ts2.8", "@types/sprintf-js@ts2.8", "@types/sqlstring@ts2.8", "@types/sshpk@ts2.8", "@types/stack-utils@ts2.8", "@types/stat-mode@ts2.8", "@types/statuses@ts2.8", "@types/strict-uri-encode@ts2.8", "@types/string-template@ts2.8", "@types/strip-ansi@ts2.8", "@types/strip-bom@ts2.8", "@types/strip-json-comments@ts2.8", "@types/supports-color@ts2.8", "@types/svg2png@ts2.8", "@types/svgo@ts2.8", "@types/table@ts2.8", "@types/tapable@ts2.8", "@types/tar@ts2.8", "@types/temp@ts2.8", "@types/tempfile@ts2.8", "@types/through@ts2.8", "@types/through2@ts2.8", "@types/tinycolor2@ts2.8", "@types/tmp@ts2.8", "@types/to-absolute-glob@ts2.8", "@types/tough-cookie@ts2.8", "@types/trim@ts2.8", "@types/tryer@ts2.8", "@types/type-check@ts2.8", "@types/type-is@ts2.8", "@types/ua-parser-js@ts2.8", "@types/uglify-js@ts2.8", "@types/uglifyjs-webpack-plugin@ts2.8", "@types/underscore@ts2.8", "@types/uniq@ts2.8", "@types/uniqid@ts2.8", "@types/untildify@ts2.8", "@types/urijs@ts2.8", "@types/url-join@ts2.8", "@types/url-parse@ts2.8", "@types/url-regex@ts2.8", "@types/user-home@ts2.8", "@types/util-deprecate@ts2.8", "@types/util.promisify@ts2.8", "@types/utils-merge@ts2.8", "@types/uuid@ts2.8", "@types/vali-date@ts2.8", "@types/vary@ts2.8", "@types/verror@ts2.8", "@types/vinyl@ts2.8", "@types/vinyl-fs@ts2.8", "@types/warning@ts2.8", "@types/watch@ts2.8", "@types/watchpack@ts2.8", "@types/webpack-dev-middleware@ts2.8", "@types/webpack-sources@ts2.8", "@types/which@ts2.8", "@types/window-size@ts2.8", "@types/wrap-ansi@ts2.8", "@types/write-file-atomic@ts2.8", "@types/ws@ts2.8", "@types/xml2js@ts2.8", "@types/xmlbuilder@ts2.8", "@types/xtend@ts2.8", "@types/yallist@ts2.8", "@types/yargs@ts2.8", "@types/yauzl@ts2.8", "@types/yeoman-generator@ts2.8", "@types/zen-observable@ts2.8", "@types/react-content-loader@ts2.8"]; const expectedCommands = [ @@ -1704,7 +1704,7 @@ namespace ts.projectSystem { }); }); - describe("typingsInstaller:: recomputing resolutions of unresolved imports", () => { + describe("tsserver:: typingsInstaller:: recomputing resolutions of unresolved imports", () => { const globalTypingsCacheLocation = "/tmp"; const appPath = "/a/b/app.js" as Path; const foooPath = "/a/b/node_modules/fooo/index.d.ts"; @@ -1775,7 +1775,7 @@ namespace ts.projectSystem { }); }); - describe("typingsInstaller:: tsserver:: with inferred Project", () => { + describe("tsserver:: typingsInstaller:: tsserver:: with inferred Project", () => { it("when projectRootPath is provided", () => { const projects = "/users/username/projects"; const projectRootPath = `${projects}/san2`; diff --git a/src/testRunner/unittests/tsserver/untitledFiles.ts b/src/testRunner/unittests/tsserver/untitledFiles.ts new file mode 100644 index 00000000000..2c429707a94 --- /dev/null +++ b/src/testRunner/unittests/tsserver/untitledFiles.ts @@ -0,0 +1,45 @@ +namespace ts.projectSystem { + describe("tsserver:: Untitled files", () => { + it("Can convert positions to locations", () => { + const aTs: File = { path: "/proj/a.ts", content: "" }; + const tsconfig: File = { path: "/proj/tsconfig.json", content: "{}" }; + const session = createSession(createServerHost([aTs, tsconfig])); + + openFilesForSession([aTs], session); + + const untitledFile = "untitled:^Untitled-1"; + executeSessionRequestNoResponse(session, protocol.CommandTypes.Open, { + file: untitledFile, + fileContent: `/// \nlet foo = 1;\nfooo/**/`, + scriptKindName: "TS", + projectRootPath: "/proj", + }); + + const response = executeSessionRequest(session, protocol.CommandTypes.GetCodeFixes, { + file: untitledFile, + startLine: 3, + startOffset: 1, + endLine: 3, + endOffset: 5, + errorCodes: [Diagnostics.Cannot_find_name_0_Did_you_mean_1.code], + }); + assert.deepEqual | undefined>(response, [ + { + description: "Change spelling to 'foo'", + fixAllDescription: "Fix all detected spelling errors", + fixId: "fixSpelling", + fixName: "spelling", + changes: [{ + fileName: untitledFile, + textChanges: [{ + start: { line: 3, offset: 1 }, + end: { line: 3, offset: 5 }, + newText: "foo", + }], + }], + commands: undefined, + }, + ]); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/versionCache.ts b/src/testRunner/unittests/tsserver/versionCache.ts index cc534b6fa5b..bd75b2bbaf4 100644 --- a/src/testRunner/unittests/tsserver/versionCache.ts +++ b/src/testRunner/unittests/tsserver/versionCache.ts @@ -15,7 +15,7 @@ namespace ts { assert.equal(editedText, checkText); } - describe(`VersionCache TS code`, () => { + describe(`tsserver:: VersionCache TS code`, () => { let validateEditAtLineCharIndex: (line: number, char: number, deleteLength: number, insertString: string) => void; before(() => { @@ -77,7 +77,7 @@ var q:Point=p;`; }); }); - describe(`VersionCache simple text`, () => { + describe(`tsserver:: VersionCache simple text`, () => { let validateEditAtPosition: (position: number, deleteLength: number, insertString: string) => void; let testContent: string; let lines: string[]; @@ -181,7 +181,7 @@ and grew 1cm per day`; }); }); - describe(`VersionCache stress test`, () => { + describe(`tsserver:: VersionCache stress test`, () => { let rsa: number[] = []; let la: number[] = []; let las: number[] = []; diff --git a/src/testRunner/unittests/tsserver/watchEnvironment.ts b/src/testRunner/unittests/tsserver/watchEnvironment.ts index 8bce3505626..5ca4d6f4570 100644 --- a/src/testRunner/unittests/tsserver/watchEnvironment.ts +++ b/src/testRunner/unittests/tsserver/watchEnvironment.ts @@ -1,6 +1,6 @@ namespace ts.projectSystem { import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; - describe("watchEnvironment:: tsserverProjectSystem watchDirectories implementation", () => { + describe("tsserver:: watchEnvironment:: tsserverProjectSystem watchDirectories implementation", () => { function verifyCompletionListWithNewFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { const projectFolder = "/a/username/project"; const projectSrcFolder = `${projectFolder}/src`; @@ -83,7 +83,7 @@ namespace ts.projectSystem { }); }); - describe("watchEnvironment:: tsserverProjectSystem Watched recursive directories with windows style file system", () => { + describe("tsserver:: watchEnvironment:: tsserverProjectSystem Watched recursive directories with windows style file system", () => { function verifyWatchedDirectories(rootedPath: string, useProjectAtRoot: boolean) { const root = useProjectAtRoot ? rootedPath : `${rootedPath}myfolder/allproject/`; const configFile: File = { diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 85be445171a..1f851a10c47 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -1,35 +1,5 @@ namespace ts.projectSystem { - function checkOpenFiles(projectService: server.ProjectService, expectedFiles: File[]) { - checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path)); - } - - function checkScriptInfos(projectService: server.ProjectService, expectedFiles: ReadonlyArray) { - checkArray("ScriptInfos files", arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles); - } - - function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs { - return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) }; - } - function protocolFileSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileSpan { - return { file: file.path, ...protocolTextSpanFromSubstring(file.content, substring, options) }; - } - function documentSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): DocumentSpan { - return { fileName: file.path, textSpan: textSpanFromSubstring(file.content, substring, options) }; - } - function renameLocation(file: File, substring: string, options?: SpanFromSubstringOptions): RenameLocation { - return documentSpanFromSubstring(file, substring, options); - } - describe("tsserverProjectSystem general functionality", () => { - const commonFile1: File = { - path: "/a/b/commonFile1.ts", - content: "let x = 1" - }; - const commonFile2: File = { - path: "/a/b/commonFile2.ts", - content: "let y = 1" - }; - it("create inferred project", () => { const appFile: File = { path: "/a/b/c/app.ts", @@ -3046,7 +3016,7 @@ var x = 10;` }; const tsconfig: File = { path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { } }), + content: JSON.stringify({ compilerOptions: {} }), }; const host = createServerHost([file, tsconfig]); const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); @@ -3115,7 +3085,7 @@ var x = 10;` }; const tsconfig: File = { path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify({ }), + content: JSON.stringify({}), }; const host = createServerHost([file, tsconfig]); const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); @@ -3228,249 +3198,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem autoDiscovery", () => { - it("does not depend on extension", () => { - const file1 = { - path: "/a/b/app.html", - content: "" - }; - const file2 = { - path: "/a/b/app.d.ts", - content: "" - }; - const host = createServerHost([file1, file2]); - const projectService = createProjectService(host); - projectService.openExternalProject({ - projectFileName: "/a/b/proj.csproj", - rootFiles: [toExternalFile(file2.path), { fileName: file1.path, hasMixedContent: true, scriptKind: ScriptKind.JS }], - options: {} - }); - projectService.checkNumberOfProjects({ externalProjects: 1 }); - const typeAcquisition = projectService.externalProjects[0].getTypeAcquisition(); - assert.isTrue(typeAcquisition.enable, "Typine acquisition should be enabled"); - }); - }); - - describe("tsserverProjectSystem navigate-to for javascript project", () => { - function containsNavToItem(items: protocol.NavtoItem[], itemName: string, itemKind: string) { - return find(items, item => item.name === itemName && item.kind === itemKind) !== undefined; - } - - it("should not include type symbols", () => { - const file1: File = { - path: "/a/b/file1.js", - content: "function foo() {}" - }; - const configFile: File = { - path: "/a/b/jsconfig.json", - content: "{}" - }; - const host = createServerHost([file1, configFile, libFile]); - const session = createSession(host); - openFilesForSession([file1], session); - - // 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 = 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 = session.executeCommand(localFunctionNavToRequst).response as protocol.NavtoItem[]; - assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`); - }); - }); - - describe("tsserverProjectSystem prefer typings to js", () => { - it("during second resolution pass", () => { - const typingsCacheLocation = "/a/typings"; - const f1 = { - path: "/a/b/app.js", - content: "var x = require('bar')" - }; - const barjs = { - path: "/a/b/node_modules/bar/index.js", - content: "export let x = 1" - }; - const barTypings = { - path: `${typingsCacheLocation}/node_modules/@types/bar/index.d.ts`, - content: "export let y: number" - }; - const config = { - path: "/a/b/jsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: true }, exclude: ["node_modules"] }) - }; - const host = createServerHost([f1, barjs, barTypings, config]); - const projectService = createProjectService(host, { typingsInstaller: new TestTypingsInstaller(typingsCacheLocation, /*throttleLimit*/ 5, host) }); - - projectService.openClientFile(f1.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, barTypings.path, config.path]); - }); - }); - - describe("tsserverProjectSystem format settings", () => { - it("can be set globally", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x;" - }; - const host = createServerHost([f1]); - const projectService = createProjectService(host); - projectService.openClientFile(f1.path); - - const defaultSettings = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath); - - // set global settings - const newGlobalSettings1 = { ...defaultSettings, placeOpenBraceOnNewLineForControlBlocks: !defaultSettings.placeOpenBraceOnNewLineForControlBlocks }; - projectService.setHostConfiguration({ formatOptions: newGlobalSettings1 }); - - // get format options for file - should be equal to new global settings - const s1 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path)); - assert.deepEqual(s1, newGlobalSettings1, "file settings should be the same with global settings"); - - // set per file format options - const newPerFileSettings = { ...defaultSettings, insertSpaceAfterCommaDelimiter: !defaultSettings.insertSpaceAfterCommaDelimiter }; - projectService.setHostConfiguration({ formatOptions: newPerFileSettings, file: f1.path }); - - // get format options for file - should be equal to new per-file settings - const s2 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path)); - assert.deepEqual(s2, newPerFileSettings, "file settings should be the same with per-file settings"); - - // set new global settings - they should not affect ones that were set per-file - const newGlobalSettings2 = { ...defaultSettings, insertSpaceAfterSemicolonInForStatements: !defaultSettings.insertSpaceAfterSemicolonInForStatements }; - projectService.setHostConfiguration({ formatOptions: newGlobalSettings2 }); - - // get format options for file - should be equal to new per-file settings - const s3 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path)); - assert.deepEqual(s3, newPerFileSettings, "file settings should still be the same with per-file settings"); - }); - }); - - describe("tsserverProjectSystem Open-file", () => { - it("can be reloaded with empty content", () => { - const f = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const projectFileName = "externalProject"; - const host = createServerHost([f]); - const projectService = createProjectService(host); - // create a project - projectService.openExternalProject({ projectFileName, rootFiles: [toExternalFile(f.path)], options: {} }); - projectService.checkNumberOfProjects({ externalProjects: 1 }); - - const p = projectService.externalProjects[0]; - // force to load the content of the file - p.updateGraph(); - - const scriptInfo = p.getScriptInfo(f.path)!; - checkSnapLength(scriptInfo.getSnapshot(), f.content.length); - - // open project and replace its content with empty string - projectService.openClientFile(f.path, ""); - checkSnapLength(scriptInfo.getSnapshot(), 0); - }); - function checkSnapLength(snap: IScriptSnapshot, expectedLength: number) { - assert.equal(snap.getLength(), expectedLength, "Incorrect snapshot size"); - } - - function verifyOpenFileWorks(useCaseSensitiveFileNames: boolean) { - const file1: File = { - path: "/a/b/src/app.ts", - content: "let x = 10;" - }; - const file2: File = { - path: "/a/B/lib/module2.ts", - content: "let z = 10;" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: "" - }; - const configFile2: File = { - path: "/a/tsconfig.json", - content: "" - }; - const host = createServerHost([file1, file2, configFile, configFile2], { - useCaseSensitiveFileNames - }); - const service = createProjectService(host); - - // Open file1 -> configFile - verifyConfigFileName(file1, "/a", configFile); - verifyConfigFileName(file1, "/a/b", configFile); - verifyConfigFileName(file1, "/a/B", configFile); - - // Open file2 use root "/a/b" - verifyConfigFileName(file2, "/a", useCaseSensitiveFileNames ? configFile2 : configFile); - verifyConfigFileName(file2, "/a/b", useCaseSensitiveFileNames ? configFile2 : configFile); - verifyConfigFileName(file2, "/a/B", useCaseSensitiveFileNames ? undefined : configFile); - - function verifyConfigFileName(file: File, projectRoot: string, expectedConfigFile: File | undefined) { - const { configFileName } = service.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRoot); - assert.equal(configFileName, expectedConfigFile && expectedConfigFile.path); - service.closeClientFile(file.path); - } - } - it("works when project root is used with case-sensitive system", () => { - verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ true); - }); - - it("works when project root is used with case-insensitive system", () => { - verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ false); - }); - - it("uses existing project even if project refresh is pending", () => { - const projectFolder = "/user/someuser/projects/myproject"; - const aFile: File = { - path: `${projectFolder}/src/a.ts`, - content: "export const x = 0;" - }; - const configFile: File = { - path: `${projectFolder}/tsconfig.json`, - content: "{}" - }; - const files = [aFile, configFile, libFile]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(aFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder); - verifyProject(); - - const bFile: File = { - path: `${projectFolder}/src/b.ts`, - content: `export {}; declare module "./a" { export const y: number; }` - }; - files.push(bFile); - host.reloadFS(files); - service.openClientFile(bFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder); - verifyProject(); - - function verifyProject() { - assert.isDefined(service.configuredProjects.get(configFile.path)); - const project = service.configuredProjects.get(configFile.path)!; - checkProjectActualFiles(project, files.map(f => f.path)); - } - }); - }); - - describe("tsserverProjectSystem Language service", () => { - it("should work correctly on case-sensitive file systems", () => { - const lib = { - path: "/a/Lib/lib.d.ts", - content: "let x: number" - }; - const f = { - path: "/a/b/app.ts", - content: "let x = 1;" - }; - const host = createServerHost([lib, f], { executingFilePath: "/a/Lib/tsc.js", useCaseSensitiveFileNames: true }); - const projectService = createProjectService(host); - projectService.openClientFile(f.path); - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - projectService.inferredProjects[0].getLanguageService().getProgram(); - }); - }); - describe("tsserverProjectSystem non-existing directories listed in config file input array", () => { it("should be tolerated without crashing the server", () => { const configFile = { @@ -3551,2912 +3278,4 @@ var x = 10;` projectService.checkNumberOfProjects({ configuredProjects: 0, inferredProjects: 1 }); }); }); - - describe("tsserverProjectSystem Inferred projects", () => { - it("should support files without extensions", () => { - const f = { - path: "/a/compile", - content: "let x = 1" - }; - const host = createServerHost([f]); - const session = createSession(host); - session.executeCommand({ - seq: 1, - type: "request", - command: "compilerOptionsForInferredProjects", - arguments: { - options: { - allowJs: true - } - } - }); - session.executeCommand({ - seq: 2, - type: "request", - command: "open", - arguments: { - file: f.path, - fileContent: f.content, - scriptKindName: "JS" - } - }); - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [f.path]); - }); - - it("inferred projects per project root", () => { - const file1 = { path: "/a/file1.ts", content: "let x = 1;", projectRootPath: "/a" }; - const file2 = { path: "/a/file2.ts", content: "let y = 2;", projectRootPath: "/a" }; - const file3 = { path: "/b/file2.ts", content: "let x = 3;", projectRootPath: "/b" }; - const file4 = { path: "/c/file3.ts", content: "let z = 4;" }; - const host = createServerHost([file1, file2, file3, file4]); - const session = createSession(host, { - useSingleInferredProject: true, - useInferredProjectPerProjectRoot: true - }); - session.executeCommand({ - seq: 1, - type: "request", - command: CommandNames.CompilerOptionsForInferredProjects, - arguments: { - options: { - allowJs: true, - target: ScriptTarget.ESNext - } - } - }); - session.executeCommand({ - seq: 2, - type: "request", - command: CommandNames.CompilerOptionsForInferredProjects, - arguments: { - options: { - allowJs: true, - target: ScriptTarget.ES2015 - }, - projectRootPath: "/b" - } - }); - session.executeCommand({ - seq: 3, - type: "request", - command: CommandNames.Open, - arguments: { - file: file1.path, - fileContent: file1.content, - scriptKindName: "JS", - projectRootPath: file1.projectRootPath - } - }); - session.executeCommand({ - seq: 4, - type: "request", - command: CommandNames.Open, - arguments: { - file: file2.path, - fileContent: file2.content, - scriptKindName: "JS", - projectRootPath: file2.projectRootPath - } - }); - session.executeCommand({ - seq: 5, - type: "request", - command: CommandNames.Open, - arguments: { - file: file3.path, - fileContent: file3.content, - scriptKindName: "JS", - projectRootPath: file3.projectRootPath - } - }); - session.executeCommand({ - seq: 6, - type: "request", - command: CommandNames.Open, - arguments: { - file: file4.path, - fileContent: file4.content, - scriptKindName: "JS" - } - }); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { inferredProjects: 3 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]); - checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); - assert.equal(projectService.inferredProjects[0].getCompilationSettings().target, ScriptTarget.ESNext); - assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ScriptTarget.ESNext); - assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ScriptTarget.ES2015); - }); - - function checkInferredProject(inferredProject: server.InferredProject, actualFiles: File[], target: ScriptTarget) { - checkProjectActualFiles(inferredProject, actualFiles.map(f => f.path)); - assert.equal(inferredProject.getCompilationSettings().target, target); - } - - function verifyProjectRootWithCaseSensitivity(useCaseSensitiveFileNames: boolean) { - const files: [File, File, File, File] = [ - { 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: [File[], 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("tsserverProjectSystem import helpers", () => { - it("should not crash in tsserver", () => { - const f1 = { - path: "/a/app.ts", - content: "export async function foo() { return 100; }" - }; - const tslib = { - path: "/a/node_modules/tslib/index.d.ts", - content: "" - }; - const host = createServerHost([f1, tslib]); - const service = createProjectService(host); - service.openExternalProject({ projectFileName: "p", rootFiles: [toExternalFile(f1.path)], options: { importHelpers: true } }); - service.checkNumberOfProjects({ externalProjects: 1 }); - }); - }); - - describe("tsserverProjectSystem searching for config file", () => { - it("should stop at projectRootPath if given", () => { - const f1 = { - path: "/a/file1.ts", - content: "" - }; - const configFile = { - path: "/tsconfig.json", - content: "{}" - }; - const host = createServerHost([f1, configFile]); - const service = createProjectService(host); - service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a"); - - checkNumberOfConfiguredProjects(service, 0); - checkNumberOfInferredProjects(service, 1); - - service.closeClientFile(f1.path); - service.openClientFile(f1.path); - checkNumberOfConfiguredProjects(service, 1); - checkNumberOfInferredProjects(service, 0); - }); - - it("should use projectRootPath when searching for inferred project again", () => { - const projectDir = "/a/b/projects/project"; - const configFileLocation = `${projectDir}/src`; - const f1 = { - path: `${configFileLocation}/file1.ts`, - content: "" - }; - const configFile = { - path: `${configFileLocation}/tsconfig.json`, - content: "{}" - }; - const configFile2 = { - path: "/a/b/projects/tsconfig.json", - content: "{}" - }; - const host = createServerHost([f1, libFile, configFile, configFile2]); - const service = createProjectService(host); - service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); - checkNumberOfProjects(service, { configuredProjects: 1 }); - assert.isDefined(service.configuredProjects.get(configFile.path)); - checkWatchedFiles(host, [libFile.path, configFile.path]); - checkWatchedDirectories(host, [], /*recursive*/ false); - const typeRootLocations = getTypeRootsFromLocation(configFileLocation); - checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true); - - // Delete config file - should create inferred project and not configured project - host.reloadFS([f1, libFile, configFile2]); - host.runQueuedTimeoutCallbacks(); - checkNumberOfProjects(service, { inferredProjects: 1 }); - checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true); - }); - - it("should use projectRootPath when searching for inferred project again 2", () => { - const projectDir = "/a/b/projects/project"; - const configFileLocation = `${projectDir}/src`; - const f1 = { - path: `${configFileLocation}/file1.ts`, - content: "" - }; - const configFile = { - path: `${configFileLocation}/tsconfig.json`, - content: "{}" - }; - const configFile2 = { - path: "/a/b/projects/tsconfig.json", - content: "{}" - }; - const host = createServerHost([f1, libFile, configFile, configFile2]); - const service = createProjectService(host, { useSingleInferredProject: true }, { useInferredProjectPerProjectRoot: true }); - service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); - checkNumberOfProjects(service, { configuredProjects: 1 }); - assert.isDefined(service.configuredProjects.get(configFile.path)); - checkWatchedFiles(host, [libFile.path, configFile.path]); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true); - - // Delete config file - should create inferred project with project root path set - host.reloadFS([f1, libFile, configFile2]); - host.runQueuedTimeoutCallbacks(); - checkNumberOfProjects(service, { inferredProjects: 1 }); - assert.equal(service.inferredProjects[0].projectRootPath, projectDir); - checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true); - }); - - describe("when the opened file is not from project root", () => { - const projectRoot = "/a/b/projects/project"; - const file: File = { - path: `${projectRoot}/src/index.ts`, - content: "let y = 10" - }; - const tsconfig: File = { - path: `${projectRoot}/tsconfig.json`, - content: "{}" - }; - const files = [file, libFile]; - const filesWithConfig = files.concat(tsconfig); - const dirOfFile = getDirectoryPath(file.path); - - function openClientFile(files: File[]) { - const host = createServerHost(files); - const projectService = createProjectService(host); - - projectService.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a/b/projects/proj"); - return { host, projectService }; - } - - function verifyConfiguredProject(host: TestServerHost, projectService: TestProjectService, orphanInferredProject?: boolean) { - projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: orphanInferredProject ? 1 : 0 }); - const project = Debug.assertDefined(projectService.configuredProjects.get(tsconfig.path)); - - if (orphanInferredProject) { - const inferredProject = projectService.inferredProjects[0]; - assert.isTrue(inferredProject.isOrphan()); - } - - checkProjectActualFiles(project, [file.path, libFile.path, tsconfig.path]); - checkWatchedFiles(host, [libFile.path, tsconfig.path]); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectories(host, (orphanInferredProject ? [projectRoot, `${dirOfFile}/node_modules/@types`] : [projectRoot]).concat(getTypeRootsFromLocation(projectRoot)), /*recursive*/ true); - } - - function verifyInferredProject(host: TestServerHost, projectService: TestProjectService) { - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - const project = projectService.inferredProjects[0]; - assert.isDefined(project); - - const filesToWatch = [libFile.path]; - forEachAncestorDirectory(dirOfFile, ancestor => { - filesToWatch.push(combinePaths(ancestor, "tsconfig.json")); - filesToWatch.push(combinePaths(ancestor, "jsconfig.json")); - }); - - checkProjectActualFiles(project, [file.path, libFile.path]); - checkWatchedFiles(host, filesToWatch); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectories(host, getTypeRootsFromLocation(dirOfFile), /*recursive*/ true); - } - - it("tsconfig for the file exists", () => { - const { host, projectService } = openClientFile(filesWithConfig); - verifyConfiguredProject(host, projectService); - - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - verifyInferredProject(host, projectService); - - host.reloadFS(filesWithConfig); - host.runQueuedTimeoutCallbacks(); - verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); - }); - - it("tsconfig for the file does not exist", () => { - const { host, projectService } = openClientFile(files); - verifyInferredProject(host, projectService); - - host.reloadFS(filesWithConfig); - host.runQueuedTimeoutCallbacks(); - verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); - - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - verifyInferredProject(host, projectService); - }); - }); - }); - - describe("tsserverProjectSystem cancellationToken", () => { - // Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test - let oldPrepare: AnyFunction; - before(() => { - oldPrepare = (Error as any).prepareStackTrace; - delete (Error as any).prepareStackTrace; - }); - - after(() => { - (Error as any).prepareStackTrace = oldPrepare; - }); - - it("is attached to request", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let xyz = 1;" - }; - const host = createServerHost([f1]); - let expectedRequestId: number; - const cancellationToken: server.ServerCancellationToken = { - isCancellationRequested: () => false, - setRequest: requestId => { - if (expectedRequestId === undefined) { - assert.isTrue(false, "unexpected call"); - } - assert.equal(requestId, expectedRequestId); - }, - resetRequest: noop - }; - - const session = createSession(host, { cancellationToken }); - - expectedRequestId = session.getNextSeq(); - session.executeCommandSeq({ - command: "open", - arguments: { file: f1.path } - }); - - expectedRequestId = session.getNextSeq(); - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - }); - - expectedRequestId = session.getNextSeq(); - session.executeCommandSeq({ - command: "occurrences", - arguments: { file: f1.path, line: 1, offset: 6 } - }); - - expectedRequestId = 2; - host.runQueuedImmediateCallbacks(); - expectedRequestId = 2; - host.runQueuedImmediateCallbacks(); - }); - - it("Geterr is cancellable", () => { - const f1 = { - path: "/a/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: {} - }) - }; - - const cancellationToken = new TestServerCancellationToken(); - const host = createServerHost([f1, config]); - const session = createSession(host, { - canUseEvents: true, - eventHandler: noop, - cancellationToken - }); - { - session.executeCommandSeq({ - command: "open", - arguments: { file: f1.path } - }); - // send geterr for missing file - session.executeCommandSeq({ - command: "geterr", - arguments: { files: ["/a/missing"] } - }); - // no files - expect 'completed' event - assert.equal(host.getOutput().length, 1, "expect 1 message"); - verifyRequestCompleted(session.getSeq(), 0); - } - { - const getErrId = session.getNextSeq(); - // send geterr for a valid file - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - }); - - assert.equal(host.getOutput().length, 0, "expect 0 messages"); - - // run new request - session.executeCommandSeq({ - command: "projectInfo", - arguments: { file: f1.path } - }); - session.clearMessages(); - - // cancel previously issued Geterr - cancellationToken.setRequestToCancel(getErrId); - host.runQueuedTimeoutCallbacks(); - - assert.equal(host.getOutput().length, 1, "expect 1 message"); - verifyRequestCompleted(getErrId, 0); - - cancellationToken.resetToken(); - } - { - const getErrId = session.getNextSeq(); - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - }); - assert.equal(host.getOutput().length, 0, "expect 0 messages"); - - // run first step - host.runQueuedTimeoutCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 message"); - const e1 = getMessage(0); - assert.equal(e1.event, "syntaxDiag"); - session.clearMessages(); - - cancellationToken.setRequestToCancel(getErrId); - host.runQueuedImmediateCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 message"); - verifyRequestCompleted(getErrId, 0); - - cancellationToken.resetToken(); - } - { - const getErrId = session.getNextSeq(); - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - }); - assert.equal(host.getOutput().length, 0, "expect 0 messages"); - - // run first step - host.runQueuedTimeoutCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 message"); - const e1 = getMessage(0); - assert.equal(e1.event, "syntaxDiag"); - session.clearMessages(); - - // the semanticDiag message - host.runQueuedImmediateCallbacks(); - assert.equal(host.getOutput().length, 1); - const e2 = getMessage(0); - assert.equal(e2.event, "semanticDiag"); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - assert.equal(host.getOutput().length, 2); - const e3 = getMessage(0); - assert.equal(e3.event, "suggestionDiag"); - verifyRequestCompleted(getErrId, 1); - - cancellationToken.resetToken(); - } - { - const getErr1 = session.getNextSeq(); - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - }); - assert.equal(host.getOutput().length, 0, "expect 0 messages"); - // run first step - host.runQueuedTimeoutCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 message"); - const e1 = getMessage(0); - assert.equal(e1.event, "syntaxDiag"); - session.clearMessages(); - - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - }); - // make sure that getErr1 is completed - verifyRequestCompleted(getErr1, 0); - } - - function verifyRequestCompleted(expectedSeq: number, n: number) { - const event = getMessage(n); - assert.equal(event.event, "requestCompleted"); - assert.equal(event.body.request_seq, expectedSeq, "expectedSeq"); - session.clearMessages(); - } - - function getMessage(n: number) { - return JSON.parse(server.extractMessage(host.getOutput()[n])); - } - }); - - it("Lower priority tasks are cancellable", () => { - const f1 = { - path: "/a/app.ts", - content: `{ let x = 1; } var foo = "foo"; var bar = "bar"; var fooBar = "fooBar";` - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: {} - }) - }; - const cancellationToken = new TestServerCancellationToken(/*cancelAfterRequest*/ 3); - const host = createServerHost([f1, config]); - const session = createSession(host, { - canUseEvents: true, - eventHandler: noop, - cancellationToken, - throttleWaitMilliseconds: 0 - }); - { - session.executeCommandSeq({ - command: "open", - arguments: { file: f1.path } - }); - - // send navbar request (normal priority) - session.executeCommandSeq({ - command: "navbar", - arguments: { file: f1.path } - }); - - // ensure the nav bar request can be canceled - verifyExecuteCommandSeqIsCancellable({ - command: "navbar", - arguments: { file: f1.path } - }); - - // send outlining spans request (normal priority) - session.executeCommandSeq({ - command: "outliningSpans", - arguments: { file: f1.path } - }); - - // ensure the outlining spans request can be canceled - verifyExecuteCommandSeqIsCancellable({ - command: "outliningSpans", - arguments: { file: f1.path } - }); - } - - function verifyExecuteCommandSeqIsCancellable(request: Partial) { - // Set the next request to be cancellable - // The cancellation token will cancel the request the third time - // isCancellationRequested() is called. - cancellationToken.setRequestToCancel(session.getNextSeq()); - let operationCanceledExceptionThrown = false; - - try { - session.executeCommandSeq(request); - } - catch (e) { - assert(e instanceof OperationCanceledException); - operationCanceledExceptionThrown = true; - } - assert(operationCanceledExceptionThrown, "Operation Canceled Exception not thrown for request: " + JSON.stringify(request)); - } - }); - }); - - describe("tsserverProjectSystem occurence highlight on string", () => { - it("should be marked if only on string values", () => { - const file1: File = { - path: "/a/b/file1.ts", - content: `let t1 = "div";\nlet t2 = "div";\nlet t3 = { "div": 123 };\nlet t4 = t3["div"];` - }; - - const host = createServerHost([file1]); - const session = createSession(host); - const projectService = session.getProjectService(); - - projectService.openClientFile(file1.path); - { - const highlightRequest = makeSessionRequest( - CommandNames.Occurrences, - { file: file1.path, line: 1, offset: 11 } - ); - const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]; - const firstOccurence = highlightResponse[0]; - assert.isTrue(firstOccurence.isInString, "Highlights should be marked with isInString"); - } - - { - const highlightRequest = makeSessionRequest( - CommandNames.Occurrences, - { file: file1.path, line: 3, offset: 13 } - ); - const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]; - assert.isTrue(highlightResponse.length === 2); - const firstOccurence = highlightResponse[0]; - assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on property name"); - } - - { - const highlightRequest = makeSessionRequest( - CommandNames.Occurrences, - { file: file1.path, line: 4, offset: 14 } - ); - const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]; - assert.isTrue(highlightResponse.length === 2); - const firstOccurence = highlightResponse[0]; - assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on indexer"); - } - }); - }); - - describe("tsserverProjectSystem maxNodeModuleJsDepth for inferred projects", () => { - it("should be set to 2 if the project has js root files", () => { - const file1: File = { - path: "/a/b/file1.js", - content: `var t = require("test"); t.` - }; - const moduleFile: File = { - path: "/a/b/node_modules/test/index.js", - content: `var v = 10; module.exports = v;` - }; - - const host = createServerHost([file1, moduleFile]); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - - let project = projectService.inferredProjects[0]; - let options = project.getCompilationSettings(); - assert.isTrue(options.maxNodeModuleJsDepth === 2); - - // Assert the option sticks - projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES2016 }); - project = projectService.inferredProjects[0]; - options = project.getCompilationSettings(); - assert.isTrue(options.maxNodeModuleJsDepth === 2); - }); - - it("should return to normal state when all js root files are removed from project", () => { - const file1 = { - path: "/a/file1.ts", - content: "let x =1;" - }; - const file2 = { - path: "/a/file2.js", - content: "let x =1;" - }; - - const host = createServerHost([file1, file2, libFile]); - const projectService = createProjectService(host, { useSingleInferredProject: true }); - - projectService.openClientFile(file1.path); - checkNumberOfInferredProjects(projectService, 1); - let project = projectService.inferredProjects[0]; - assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth); - - projectService.openClientFile(file2.path); - project = projectService.inferredProjects[0]; - assert.isTrue(project.getCompilationSettings().maxNodeModuleJsDepth === 2); - - projectService.closeClientFile(file2.path); - project = projectService.inferredProjects[0]; - assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth); - }); - }); - - describe("tsserverProjectSystem refactors", () => { - it("use formatting options", () => { - const file = { - path: "/a.ts", - content: "function f() {\n 1;\n}", - }; - const host = createServerHost([file]); - const session = createSession(host); - openFilesForSession([file], session); - - const response0 = session.executeCommandSeq({ - command: server.protocol.CommandTypes.Configure, - arguments: { - formatOptions: { - indentSize: 2, - }, - }, - }).response; - assert.deepEqual(response0, /*expected*/ undefined); - - const response1 = session.executeCommandSeq({ - command: server.protocol.CommandTypes.GetEditsForRefactor, - arguments: { - refactor: "Extract Symbol", - action: "function_scope_1", - file: "/a.ts", - startLine: 2, - startOffset: 3, - endLine: 2, - endOffset: 4, - }, - }).response; - assert.deepEqual(response1, { - edits: [ - { - fileName: "/a.ts", - textChanges: [ - { - start: { line: 2, offset: 3 }, - end: { line: 2, offset: 5 }, - newText: "newFunction();", - }, - { - start: { line: 3, offset: 2 }, - end: { line: 3, offset: 2 }, - newText: "\n\nfunction newFunction() {\n 1;\n}\n", - }, - ] - } - ], - renameFilename: "/a.ts", - renameLocation: { line: 2, offset: 3 }, - }); - }); - - it("handles text changes in tsconfig.json", () => { - const aTs = { - path: "/a.ts", - content: "export const a = 0;", - }; - const tsconfig = { - path: "/tsconfig.json", - content: '{ "files": ["./a.ts"] }', - }; - - const session = createSession(createServerHost([aTs, tsconfig])); - openFilesForSession([aTs], session); - - const response1 = session.executeCommandSeq({ - command: server.protocol.CommandTypes.GetEditsForRefactor, - arguments: { - refactor: "Move to a new file", - action: "Move to a new file", - file: "/a.ts", - startLine: 1, - startOffset: 1, - endLine: 1, - endOffset: 20, - }, - }).response; - assert.deepEqual(response1, { - edits: [ - { - fileName: "/a.ts", - textChanges: [ - { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 20 }, - newText: "", - }, - ], - }, - { - fileName: "/tsconfig.json", - textChanges: [ - { - start: { line: 1, offset: 21 }, - end: { line: 1, offset: 21 }, - newText: ", \"./a.1.ts\"", - }, - ], - }, - { - fileName: "/a.1.ts", - textChanges: [ - { - start: { line: 0, offset: 0 }, - end: { line: 0, offset: 0 }, - newText: "export const a = 0;\n", - }, - ], - } - ], - renameFilename: undefined, - renameLocation: undefined, - }); - }); - }); - - describe("tsserverProjectSystem forceConsistentCasingInFileNames", () => { - it("works when extends is specified with a case insensitive file system", () => { - const rootPath = "/Users/username/dev/project"; - const file1: File = { - path: `${rootPath}/index.ts`, - content: 'import {x} from "file2";', - }; - const file2: File = { - path: `${rootPath}/file2.js`, - content: "", - }; - const file2Dts: File = { - path: `${rootPath}/types/file2/index.d.ts`, - content: "export declare const x: string;", - }; - const tsconfigAll: File = { - path: `${rootPath}/tsconfig.all.json`, - content: JSON.stringify({ - compilerOptions: { - baseUrl: ".", - paths: { file2: ["./file2.js"] }, - typeRoots: ["./types"], - forceConsistentCasingInFileNames: true, - }, - }), - }; - const tsconfig: File = { - path: `${rootPath}/tsconfig.json`, - content: JSON.stringify({ extends: "./tsconfig.all.json" }), - }; - - const host = createServerHost([file1, file2, file2Dts, libFile, tsconfig, tsconfigAll], { useCaseSensitiveFileNames: false }); - const session = createSession(host); - - openFilesForSession([file1], session); - const projectService = session.getProjectService(); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - const diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); - }); - }); - - describe("tsserverProjectSystem getEditsForFileRename", () => { - it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => { - const userTs: File = { - path: "/user.ts", - content: 'import { x } from "./old";', - }; - const newTs: File = { - path: "/new.ts", - content: "export const x = 0;", - }; - const tsconfig: File = { - path: "/tsconfig.json", - content: "{}", - }; - - const host = createServerHost([userTs, newTs, tsconfig]); - const projectService = createProjectService(host); - projectService.openClientFile(userTs.path); - const project = projectService.configuredProjects.get(tsconfig.path)!; - - Debug.assert(!!project.resolveModuleNames); - - const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatSettings, emptyOptions); - assert.deepEqual>(edits, [{ - fileName: "/user.ts", - textChanges: [{ - span: textSpanFromSubstring(userTs.content, "./old"), - newText: "./new", - }], - }]); - }); - - it("works with multiple projects", () => { - const aUserTs: File = { - path: "/a/user.ts", - content: 'import { x } from "./old";', - }; - const aOldTs: File = { - path: "/a/old.ts", - content: "export const x = 0;", - }; - const aTsconfig: File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ files: ["./old.ts", "./user.ts"] }), - }; - const bUserTs: File = { - path: "/b/user.ts", - content: 'import { x } from "../a/old";', - }; - const bTsconfig: File = { - path: "/b/tsconfig.json", - content: "{}", - }; - - const host = createServerHost([aUserTs, aOldTs, aTsconfig, bUserTs, bTsconfig]); - const session = createSession(host); - openFilesForSession([aUserTs, bUserTs], session); - - const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { - oldFilePath: aOldTs.path, - newFilePath: "/a/new.ts", - }); - assert.deepEqual>(response, [ - { - fileName: aTsconfig.path, - textChanges: [{ ...protocolTextSpanFromSubstring(aTsconfig.content, "./old.ts"), newText: "new.ts" }], - }, - { - fileName: aUserTs.path, - textChanges: [{ ...protocolTextSpanFromSubstring(aUserTs.content, "./old"), newText: "./new" }], - }, - { - fileName: bUserTs.path, - textChanges: [{ ...protocolTextSpanFromSubstring(bUserTs.content, "../a/old"), newText: "../a/new" }], - }, - ]); - }); - - it("works with file moved to inferred project", () => { - const aTs: File = { path: "/a.ts", content: 'import {} from "./b";' }; - const cTs: File = { path: "/c.ts", content: "export {};" }; - const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./a.ts", "./b.ts"] }) }; - - const host = createServerHost([aTs, cTs, tsconfig]); - const session = createSession(host); - openFilesForSession([aTs, cTs], session); - - const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { - oldFilePath: "/b.ts", - newFilePath: cTs.path, - }); - assert.deepEqual>(response, [ - { - fileName: "/tsconfig.json", - textChanges: [{ ...protocolTextSpanFromSubstring(tsconfig.content, "./b.ts"), newText: "c.ts" }], - }, - { - fileName: "/a.ts", - textChanges: [{ ...protocolTextSpanFromSubstring(aTs.content, "./b"), newText: "./c" }], - }, - ]); - }); - }); - - describe("tsserverProjectSystem document registry in project service", () => { - const projectRootPath = "/user/username/projects/project"; - const importModuleContent = `import {a} from "./module1"`; - const file: File = { - path: `${projectRootPath}/index.ts`, - content: importModuleContent - }; - const moduleFile: File = { - path: `${projectRootPath}/module1.d.ts`, - content: "export const a: number;" - }; - const configFile: File = { - path: `${projectRootPath}/tsconfig.json`, - content: JSON.stringify({ files: ["index.ts"] }) - }; - - function getProject(service: TestProjectService) { - return service.configuredProjects.get(configFile.path)!; - } - - function checkProject(service: TestProjectService, moduleIsOrphan: boolean) { - // Update the project - const project = getProject(service); - project.getLanguageService(); - checkProjectActualFiles(project, [file.path, libFile.path, configFile.path, ...(moduleIsOrphan ? [] : [moduleFile.path])]); - const moduleInfo = service.getScriptInfo(moduleFile.path)!; - assert.isDefined(moduleInfo); - assert.equal(moduleInfo.isOrphan(), moduleIsOrphan); - const key = service.documentRegistry.getKeyForCompilationSettings(project.getCompilationSettings()); - assert.deepEqual(service.documentRegistry.getLanguageServiceRefCounts(moduleInfo.path), [[key, moduleIsOrphan ? undefined : 1]]); - } - - function createServiceAndHost() { - const host = createServerHost([file, moduleFile, libFile, configFile]); - const service = createProjectService(host); - service.openClientFile(file.path); - checkProject(service, /*moduleIsOrphan*/ false); - return { host, service }; - } - - function changeFileToNotImportModule(service: TestProjectService) { - const info = service.getScriptInfo(file.path)!; - service.applyChangesToFile(info, [{ span: { start: 0, length: importModuleContent.length }, newText: "" }]); - checkProject(service, /*moduleIsOrphan*/ true); - } - - function changeFileToImportModule(service: TestProjectService) { - const info = service.getScriptInfo(file.path)!; - service.applyChangesToFile(info, [{ span: { start: 0, length: 0 }, newText: importModuleContent }]); - checkProject(service, /*moduleIsOrphan*/ false); - } - - it("Caches the source file if script info is orphan", () => { - const { service } = createServiceAndHost(); - const project = getProject(service); - - const moduleInfo = service.getScriptInfo(moduleFile.path)!; - const sourceFile = moduleInfo.cacheSourceFile!.sourceFile; - assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); - - // edit file - changeFileToNotImportModule(service); - assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); - - // write content back - changeFileToImportModule(service); - assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); - assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); - }); - - it("Caches the source file if script info is orphan, and orphan script info changes", () => { - const { host, service } = createServiceAndHost(); - const project = getProject(service); - - const moduleInfo = service.getScriptInfo(moduleFile.path)!; - const sourceFile = moduleInfo.cacheSourceFile!.sourceFile; - assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); - - // edit file - changeFileToNotImportModule(service); - assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); - - const updatedModuleContent = moduleFile.content + "\nexport const b: number;"; - host.writeFile(moduleFile.path, updatedModuleContent); - - // write content back - changeFileToImportModule(service); - assert.notEqual(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); - assert.equal(project.getSourceFile(moduleInfo.path), moduleInfo.cacheSourceFile!.sourceFile); - assert.equal(moduleInfo.cacheSourceFile!.sourceFile.text, updatedModuleContent); - }); - }); - - describe("tsserverProjectSystem syntax operations", () => { - function navBarFull(session: TestSession, file: File) { - return JSON.stringify(session.executeCommandSeq({ - command: protocol.CommandTypes.NavBarFull, - arguments: { file: file.path } - }).response); - } - - function openFile(session: TestSession, file: File) { - session.executeCommandSeq({ - command: protocol.CommandTypes.Open, - arguments: { file: file.path, fileContent: file.content } - }); - } - - it("works when file is removed and added with different content", () => { - const projectRoot = "/user/username/projects/myproject"; - const app: File = { - path: `${projectRoot}/app.ts`, - content: "console.log('Hello world');" - }; - const unitTest1: File = { - path: `${projectRoot}/unitTest1.ts`, - content: `import assert = require('assert'); - -describe("Test Suite 1", () => { - it("Test A", () => { - assert.ok(true, "This shouldn't fail"); - }); - - it("Test B", () => { - assert.ok(1 === 1, "This shouldn't fail"); - assert.ok(false, "This should fail"); - }); -});` - }; - const tsconfig: File = { - path: `${projectRoot}/tsconfig.json`, - content: "{}" - }; - const files = [app, libFile, tsconfig]; - const host = createServerHost(files); - const session = createSession(host); - const service = session.getProjectService(); - openFile(session, app); - - checkNumberOfProjects(service, { configuredProjects: 1 }); - const project = service.configuredProjects.get(tsconfig.path)!; - const expectedFilesWithoutUnitTest1 = files.map(f => f.path); - checkProjectActualFiles(project, expectedFilesWithoutUnitTest1); - - host.writeFile(unitTest1.path, unitTest1.content); - host.runQueuedTimeoutCallbacks(); - const expectedFilesWithUnitTest1 = expectedFilesWithoutUnitTest1.concat(unitTest1.path); - checkProjectActualFiles(project, expectedFilesWithUnitTest1); - - openFile(session, unitTest1); - checkProjectActualFiles(project, expectedFilesWithUnitTest1); - - const navBarResultUnitTest1 = navBarFull(session, unitTest1); - host.deleteFile(unitTest1.path); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectActualFiles(project, expectedFilesWithoutUnitTest1); - - session.executeCommandSeq({ - command: protocol.CommandTypes.Close, - arguments: { file: unitTest1.path } - }); - checkProjectActualFiles(project, expectedFilesWithoutUnitTest1); - - const unitTest1WithChangedContent: File = { - path: unitTest1.path, - content: `import assert = require('assert'); - -export function Test1() { - assert.ok(true, "This shouldn't fail"); -}; - -export function Test2() { - assert.ok(1 === 1, "This shouldn't fail"); - assert.ok(false, "This should fail"); -};` - }; - host.writeFile(unitTest1.path, unitTest1WithChangedContent.content); - host.runQueuedTimeoutCallbacks(); - checkProjectActualFiles(project, expectedFilesWithUnitTest1); - - openFile(session, unitTest1WithChangedContent); - checkProjectActualFiles(project, expectedFilesWithUnitTest1); - const sourceFile = project.getLanguageService().getNonBoundSourceFile(unitTest1WithChangedContent.path); - assert.strictEqual(sourceFile.text, unitTest1WithChangedContent.content); - - const navBarResultUnitTest1WithChangedContent = navBarFull(session, unitTest1WithChangedContent); - assert.notStrictEqual(navBarResultUnitTest1WithChangedContent, navBarResultUnitTest1, "With changes in contents of unitTest file, we should see changed naviagation bar item result"); - }); - }); - - describe("tsserverProjectSystem completions", () => { - it("works", () => { - const aTs: File = { - path: "/a.ts", - content: "export const foo = 0;", - }; - const bTs: File = { - path: "/b.ts", - content: "foo", - }; - const tsconfig: File = { - path: "/tsconfig.json", - content: "{}", - }; - - const session = createSession(createServerHost([aTs, bTs, tsconfig])); - openFilesForSession([aTs, bTs], session); - - const requestLocation: protocol.FileLocationRequestArgs = { - file: bTs.path, - line: 1, - offset: 3, - }; - - const response = executeSessionRequest(session, protocol.CommandTypes.CompletionInfo, { - ...requestLocation, - includeExternalModuleExports: true, - prefix: "foo", - }); - const entry: protocol.CompletionEntry = { - hasAction: true, - insertText: undefined, - isRecommended: undefined, - kind: ScriptElementKind.constElement, - kindModifiers: ScriptElementKindModifier.exportedModifier, - name: "foo", - replacementSpan: undefined, - sortText: "0", - source: "/a", - }; - assert.deepEqual(response, { - isGlobalCompletion: true, - isMemberCompletion: false, - isNewIdentifierLocation: false, - entries: [entry], - }); - - const detailsRequestArgs: protocol.CompletionDetailsRequestArgs = { - ...requestLocation, - entryNames: [{ name: "foo", source: "/a" }], - }; - - const detailsResponse = executeSessionRequest(session, protocol.CommandTypes.CompletionDetails, detailsRequestArgs); - const detailsCommon: protocol.CompletionEntryDetails & CompletionEntryDetails = { - displayParts: [ - keywordPart(SyntaxKind.ConstKeyword), - spacePart(), - displayPart("foo", SymbolDisplayPartKind.localName), - punctuationPart(SyntaxKind.ColonToken), - spacePart(), - displayPart("0", SymbolDisplayPartKind.stringLiteral), - ], - documentation: emptyArray, - kind: ScriptElementKind.constElement, - kindModifiers: ScriptElementKindModifier.exportedModifier, - name: "foo", - source: [{ text: "./a", kind: "text" }], - tags: undefined, - }; - assert.deepEqual | undefined>(detailsResponse, [ - { - codeActions: [ - { - description: `Import 'foo' from module "./a"`, - changes: [ - { - fileName: "/b.ts", - textChanges: [ - { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: 'import { foo } from "./a";\n\n', - }, - ], - }, - ], - commands: undefined, - }, - ], - ...detailsCommon, - }, - ]); - - interface CompletionDetailsFullRequest extends protocol.FileLocationRequest { - readonly command: protocol.CommandTypes.CompletionDetailsFull; - readonly arguments: protocol.CompletionDetailsRequestArgs; - } - interface CompletionDetailsFullResponse extends protocol.Response { - readonly body?: ReadonlyArray; - } - const detailsFullResponse = executeSessionRequest(session, protocol.CommandTypes.CompletionDetailsFull, detailsRequestArgs); - assert.deepEqual | undefined>(detailsFullResponse, [ - { - codeActions: [ - { - description: `Import 'foo' from module "./a"`, - changes: [ - { - fileName: "/b.ts", - textChanges: [createTextChange(createTextSpan(0, 0), 'import { foo } from "./a";\n\n')], - }, - ], - commands: undefined, - } - ], - ...detailsCommon, - } - ]); - }); - }); - - describe("tsserverProjectSystem rename", () => { - it("works with fileToRename", () => { - const aTs: File = { path: "/a.ts", content: "export const a = 0;" }; - const bTs: File = { path: "/b.ts", content: 'import { a } from "./a";' }; - - const session = createSession(createServerHost([aTs, bTs])); - openFilesForSession([bTs], session); - - const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(bTs, 'a";')); - assert.deepEqual(response, { - info: { - canRename: true, - fileToRename: aTs.path, - displayName: aTs.path, - fullDisplayName: aTs.path, - kind: ScriptElementKind.moduleElement, - kindModifiers: "", - triggerSpan: protocolTextSpanFromSubstring(bTs.content, "a", { index: 1 }), - }, - locs: [{ file: bTs.path, locs: [protocolRenameSpanFromSubstring(bTs.content, "./a")] }], - }); - }); - - it("works with prefixText and suffixText", () => { - const aTs: File = { path: "/a.ts", content: "const x = 0; const o = { x };" }; - const session = createSession(createServerHost([aTs])); - openFilesForSession([aTs], session); - - const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "x")); - assert.deepEqual(response, { - info: { - canRename: true, - fileToRename: undefined, - displayName: "x", - fullDisplayName: "x", - kind: ScriptElementKind.constElement, - kindModifiers: ScriptElementKindModifier.none, - triggerSpan: protocolTextSpanFromSubstring(aTs.content, "x"), - }, - locs: [ - { - file: aTs.path, - locs: [ - protocolRenameSpanFromSubstring(aTs.content, "x"), - protocolRenameSpanFromSubstring(aTs.content, "x", { index: 1 }, { prefixText: "x: " }), - ], - }, - ], - }); - }); - }); - - describe("tsserverProjectSystem typeReferenceDirectives", () => { - it("when typeReferenceDirective contains UpperCasePackage", () => { - const projectLocation = "/user/username/projects/myproject"; - const libProjectLocation = `${projectLocation}/lib`; - const typeLib: File = { - path: `${libProjectLocation}/@types/UpperCasePackage/index.d.ts`, - content: `declare class BrokenTest { - constructor(name: string, width: number, height: number, onSelect: Function); - Name: string; - SelectedFile: string; -}` - }; - const appLib: File = { - path: `${libProjectLocation}/@app/lib/index.d.ts`, - content: `/// -declare class TestLib { - issue: BrokenTest; - constructor(); - test(): void; -}` - }; - const testProjectLocation = `${projectLocation}/test`; - const testFile: File = { - path: `${testProjectLocation}/test.ts`, - content: `class TestClass1 { - - constructor() { - var l = new TestLib(); - - } - - public test2() { - var x = new BrokenTest('',0,0,null); - - } -}` - }; - const testConfig: File = { - path: `${testProjectLocation}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "amd", - typeRoots: ["../lib/@types", "../lib/@app"] - } - }) - }; - - const files = [typeLib, appLib, testFile, testConfig, libFile]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(testFile.path); - checkNumberOfProjects(service, { configuredProjects: 1 }); - const project = service.configuredProjects.get(testConfig.path)!; - checkProjectActualFiles(project, files.map(f => f.path)); - host.writeFile(appLib.path, appLib.content.replace("test()", "test2()")); - host.checkTimeoutQueueLengthAndRun(2); - }); - - it("when typeReferenceDirective is relative path and in a sibling folder", () => { - const projectRootPath = "/user/username/projects/browser-addon"; - const projectPath = `${projectRootPath}/background`; - const file: File = { - path: `${projectPath}/a.ts`, - content: "let x = 10;" - }; - const tsconfig: File = { - path: `${projectPath}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - types: [ - "../typedefs/filesystem" - ] } - }) - }; - const filesystem: File = { - path: `${projectRootPath}/typedefs/filesystem.d.ts`, - content: `interface LocalFileSystem { someProperty: string; }` - }; - const files = [file, tsconfig, filesystem, libFile]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(file.path); - }); - }); - - describe("tsserverProjectSystem project references", () => { - const aTs: File = { - path: "/a/a.ts", - content: "export function fnA() {}\nexport interface IfaceA {}\nexport const instanceA: IfaceA = {};", - }; - const compilerOptions: CompilerOptions = { - outDir: "bin", - declaration: true, - declarationMap: true, - composite: true, - }; - const configContent = JSON.stringify({ compilerOptions }); - const aTsconfig: File = { path: "/a/tsconfig.json", content: configContent }; - - const aDtsMapContent: RawSourceMap = { - version: 3, - file: "a.d.ts", - sourceRoot: "", - sources: ["../a.ts"], - names: [], - mappings: "AAAA,wBAAgB,GAAG,SAAK;AACxB,MAAM,WAAW,MAAM;CAAG;AAC1B,eAAO,MAAM,SAAS,EAAE,MAAW,CAAC" - }; - const aDtsMap: File = { - path: "/a/bin/a.d.ts.map", - content: JSON.stringify(aDtsMapContent), - }; - const aDts: File = { - path: "/a/bin/a.d.ts", - // Need to mangle the sourceMappingURL part or it breaks the build - content: `export declare function fnA(): void;\nexport interface IfaceA {\n}\nexport declare const instanceA: IfaceA;\n//# source${""}MappingURL=a.d.ts.map`, - }; - - const bTs: File = { - path: "/b/b.ts", - content: "export function fnB() {}", - }; - const bTsconfig: File = { path: "/b/tsconfig.json", content: configContent }; - - const bDtsMapContent: RawSourceMap = { - version: 3, - file: "b.d.ts", - sourceRoot: "", - sources: ["../b.ts"], - names: [], - mappings: "AAAA,wBAAgB,GAAG,SAAK", - }; - const bDtsMap: File = { - path: "/b/bin/b.d.ts.map", - content: JSON.stringify(bDtsMapContent), - }; - const bDts: File = { - // Need to mangle the sourceMappingURL part or it breaks the build - path: "/b/bin/b.d.ts", - content: `export declare function fnB(): void;\n//# source${""}MappingURL=b.d.ts.map`, - }; - - const dummyFile: File = { - path: "/dummy/dummy.ts", - content: "let a = 10;" - }; - - const userTs: File = { - path: "/user/user.ts", - content: 'import * as a from "../a/bin/a";\nimport * as b from "../b/bin/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }', - }; - - const userTsForConfigProject: File = { - path: "/user/user.ts", - content: 'import * as a from "../a/a";\nimport * as b from "../b/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }', - }; - - const userTsconfig: File = { - path: "/user/tsconfig.json", - content: JSON.stringify({ - file: ["user.ts"], - references: [{ path: "../a" }, { path: "../b" }] - }) - }; - - function makeSampleProjects(addUserTsConfig?: boolean) { - const host = createServerHost([aTs, aTsconfig, aDtsMap, aDts, bTsconfig, bTs, bDtsMap, bDts, ...(addUserTsConfig ? [userTsForConfigProject, userTsconfig] : [userTs]), dummyFile]); - const session = createSession(host); - - checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); - checkDeclarationFiles(bTs, session, [bDtsMap, bDts]); - - // Testing what happens if we delete the original sources. - host.deleteFile(bTs.path); - - openFilesForSession([userTs], session); - const service = session.getProjectService(); - checkNumberOfProjects(service, addUserTsConfig ? { configuredProjects: 1 } : { inferredProjects: 1 }); - return session; - } - - function verifyInferredProjectUnchanged(session: TestSession) { - checkProjectActualFiles(session.getProjectService().inferredProjects[0], [userTs.path, aDts.path, bDts.path]); - } - - function verifyDummyProject(session: TestSession) { - checkProjectActualFiles(session.getProjectService().inferredProjects[0], [dummyFile.path]); - } - - function verifyOnlyOrphanInferredProject(session: TestSession) { - openFilesForSession([dummyFile], session); - checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); - verifyDummyProject(session); - } - - function verifySingleInferredProject(session: TestSession) { - checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); - verifyInferredProjectUnchanged(session); - - // Close user file should close all the projects after opening dummy file - closeFilesForSession([userTs], session); - verifyOnlyOrphanInferredProject(session); - } - - function verifyATsConfigProject(session: TestSession) { - checkProjectActualFiles(session.getProjectService().configuredProjects.get(aTsconfig.path)!, [aTs.path, aTsconfig.path]); - } - - function verifyATsConfigOriginalProject(session: TestSession) { - checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); - verifyInferredProjectUnchanged(session); - verifyATsConfigProject(session); - // Close user file should close all the projects - closeFilesForSession([userTs], session); - verifyOnlyOrphanInferredProject(session); - } - - function verifyATsConfigWhenOpened(session: TestSession) { - checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); - verifyInferredProjectUnchanged(session); - verifyATsConfigProject(session); - - closeFilesForSession([userTs], session); - openFilesForSession([dummyFile], session); - checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); - verifyDummyProject(session); - verifyATsConfigProject(session); // ATsConfig should still be alive - } - - function verifyUserTsConfigProject(session: TestSession) { - checkProjectActualFiles(session.getProjectService().configuredProjects.get(userTsconfig.path)!, [userTs.path, aDts.path, userTsconfig.path]); - } - - it("goToDefinition", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.Definition, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "fnA")]); - verifySingleInferredProject(session); - }); - - it("getDefinitionAndBoundSpan", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, { - textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), - definitions: [protocolFileSpanFromSubstring(aTs, "fnA")], - }); - verifySingleInferredProject(session); - }); - - it("getDefinitionAndBoundSpan with file navigation", () => { - const session = makeSampleProjects(/*addUserTsConfig*/ true); - const response = executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, { - textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), - definitions: [protocolFileSpanFromSubstring(aTs, "fnA")], - }); - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); - verifyUserTsConfigProject(session); - - // Navigate to the definition - closeFilesForSession([userTs], session); - openFilesForSession([aTs], session); - - // UserTs configured project should be alive - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); - verifyUserTsConfigProject(session); - verifyATsConfigProject(session); - - closeFilesForSession([aTs], session); - verifyOnlyOrphanInferredProject(session); - }); - - it("goToType", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.TypeDefinition, protocolFileLocationFromSubstring(userTs, "instanceA")); - assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "IfaceA")]); - verifySingleInferredProject(session); - }); - - it("goToImplementation", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.Implementation, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "fnA")]); - verifySingleInferredProject(session); - }); - - it("goToDefinition -- target does not exist", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, CommandNames.Definition, protocolFileLocationFromSubstring(userTs, "fnB()")); - // bTs does not exist, so stick with bDts - assert.deepEqual(response, [protocolFileSpanFromSubstring(bDts, "fnB")]); - verifySingleInferredProject(session); - }); - - it("navigateTo", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, CommandNames.Navto, { file: userTs.path, searchValue: "fn" }); - assert.deepEqual | undefined>(response, [ - { - ...protocolFileSpanFromSubstring(bDts, "export declare function fnB(): void;"), - name: "fnB", - matchKind: "prefix", - isCaseSensitive: true, - kind: ScriptElementKind.functionElement, - kindModifiers: "export,declare", - }, - { - ...protocolFileSpanFromSubstring(userTs, "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"), - name: "fnUser", - matchKind: "prefix", - isCaseSensitive: true, - kind: ScriptElementKind.functionElement, - kindModifiers: "export", - }, - { - ...protocolFileSpanFromSubstring(aTs, "export function fnA() {}"), - name: "fnA", - matchKind: "prefix", - isCaseSensitive: true, - kind: ScriptElementKind.functionElement, - kindModifiers: "export", - }, - ]); - - verifyATsConfigOriginalProject(session); - }); - - const referenceATs = (aTs: File): protocol.ReferencesResponseItem => makeReferenceItem(aTs, /*isDefinition*/ true, "fnA", "export function fnA() {}"); - const referencesUserTs = (userTs: File): ReadonlyArray => [ - makeReferenceItem(userTs, /*isDefinition*/ false, "fnA", "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"), - ]; - - it("findAllReferences", () => { - const session = makeSampleProjects(); - - const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, { - refs: [...referencesUserTs(userTs), referenceATs(aTs)], - symbolName: "fnA", - symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnA()").offset, - symbolDisplayString: "function fnA(): void", - }); - - verifyATsConfigOriginalProject(session); - }); - - it("findAllReferences -- starting at definition", () => { - const session = makeSampleProjects(); - openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. - const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(aTs, "fnA")); - assert.deepEqual(response, { - refs: [referenceATs(aTs), ...referencesUserTs(userTs)], - symbolName: "fnA", - symbolStartOffset: protocolLocationFromSubstring(aTs.content, "fnA").offset, - symbolDisplayString: "function fnA(): void", - }); - verifyATsConfigWhenOpened(session); - }); - - interface ReferencesFullRequest extends protocol.FileLocationRequest { readonly command: protocol.CommandTypes.ReferencesFull; } - interface ReferencesFullResponse extends protocol.Response { readonly body: ReadonlyArray; } - - it("findAllReferencesFull", () => { - const session = makeSampleProjects(); - - const responseFull = executeSessionRequest(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(userTs, "fnA()")); - - assert.deepEqual>(responseFull, [ - { - definition: { - ...documentSpanFromSubstring(aTs, "fnA"), - kind: ScriptElementKind.functionElement, - name: "function fnA(): void", - containerKind: ScriptElementKind.unknown, - containerName: "", - displayParts: [ - keywordPart(SyntaxKind.FunctionKeyword), - spacePart(), - displayPart("fnA", SymbolDisplayPartKind.functionName), - punctuationPart(SyntaxKind.OpenParenToken), - punctuationPart(SyntaxKind.CloseParenToken), - punctuationPart(SyntaxKind.ColonToken), - spacePart(), - keywordPart(SyntaxKind.VoidKeyword), - ], - }, - references: [ - makeReferenceEntry(userTs, /*isDefinition*/ false, "fnA"), - makeReferenceEntry(aTs, /*isDefinition*/ true, "fnA"), - ], - }, - ]); - verifyATsConfigOriginalProject(session); - }); - - it("findAllReferencesFull definition is in mapped file", () => { - const aTs: File = { path: "/a/a.ts", content: `function f() {}` }; - const aTsconfig: File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ compilerOptions: { declaration: true, declarationMap: true, outFile: "../bin/a.js" } }), - }; - const bTs: File = { path: "/b/b.ts", content: `f();` }; - const bTsconfig: File = { path: "/b/tsconfig.json", content: JSON.stringify({ references: [{ path: "../a" }] }) }; - const aDts: File = { path: "/bin/a.d.ts", content: `declare function f(): void;\n//# sourceMappingURL=a.d.ts.map` }; - const aDtsMap: File = { - path: "/bin/a.d.ts.map", - content: JSON.stringify({ version: 3, file: "a.d.ts", sourceRoot: "", sources: ["../a/a.ts"], names: [], mappings: "AAAA,iBAAS,CAAC,SAAK" }), - }; - - const session = createSession(createServerHost([aTs, aTsconfig, bTs, bTsconfig, aDts, aDtsMap])); - checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); - openFilesForSession([bTs], session); - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); - - const responseFull = executeSessionRequest(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(bTs, "f()")); - - assert.deepEqual>(responseFull, [ - { - definition: { - containerKind: ScriptElementKind.unknown, - containerName: "", - displayParts: [ - keywordPart(SyntaxKind.FunctionKeyword), - spacePart(), - displayPart("f", SymbolDisplayPartKind.functionName), - punctuationPart(SyntaxKind.OpenParenToken), - punctuationPart(SyntaxKind.CloseParenToken), - punctuationPart(SyntaxKind.ColonToken), - spacePart(), - keywordPart(SyntaxKind.VoidKeyword), - ], - fileName: aTs.path, - kind: ScriptElementKind.functionElement, - name: "function f(): void", - textSpan: { start: 9, length: 1 }, - }, - references: [ - { - fileName: bTs.path, - isDefinition: false, - isInString: undefined, - isWriteAccess: false, - textSpan: { start: 0, length: 1 }, - }, - { - fileName: aTs.path, - isDefinition: true, - isInString: undefined, - isWriteAccess: true, - textSpan: { start: 9, length: 1 }, - }, - ], - } - ]); - }); - - it("findAllReferences -- target does not exist", () => { - const session = makeSampleProjects(); - - const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnB()")); - assert.deepEqual(response, { - refs: [ - makeReferenceItem(bDts, /*isDefinition*/ true, "fnB", "export declare function fnB(): void;"), - makeReferenceItem(userTs, /*isDefinition*/ false, "fnB", "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"), - ], - symbolName: "fnB", - symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnB()").offset, - symbolDisplayString: "function fnB(): void", - }); - verifySingleInferredProject(session); - }); - - const renameATs = (aTs: File): protocol.SpanGroup => ({ - file: aTs.path, - locs: [protocolRenameSpanFromSubstring(aTs.content, "fnA")], - }); - const renameUserTs = (userTs: File): protocol.SpanGroup => ({ - file: userTs.path, - locs: [protocolRenameSpanFromSubstring(userTs.content, "fnA")], - }); - - it("renameLocations", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, { - info: { - canRename: true, - displayName: "fnA", - fileToRename: undefined, - fullDisplayName: '"/a/bin/a".fnA', // Ideally this would use the original source's path instead of the declaration file's path. - kind: ScriptElementKind.functionElement, - kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","), - triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), - }, - locs: [renameUserTs(userTs), renameATs(aTs)], - }); - verifyATsConfigOriginalProject(session); - }); - - it("renameLocations -- starting at definition", () => { - const session = makeSampleProjects(); - openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. - const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "fnA")); - assert.deepEqual(response, { - info: { - canRename: true, - displayName: "fnA", - fileToRename: undefined, - fullDisplayName: '"/a/a".fnA', - kind: ScriptElementKind.functionElement, - kindModifiers: ScriptElementKindModifier.exportedModifier, - triggerSpan: protocolTextSpanFromSubstring(aTs.content, "fnA"), - }, - locs: [renameATs(aTs), renameUserTs(userTs)], - }); - verifyATsConfigWhenOpened(session); - }); - - it("renameLocationsFull", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.RenameLocationsFull, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual>(response, [ - renameLocation(userTs, "fnA"), - renameLocation(aTs, "fnA"), - ]); - verifyATsConfigOriginalProject(session); - }); - - it("renameLocations -- target does not exist", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnB()")); - assert.deepEqual(response, { - info: { - canRename: true, - displayName: "fnB", - fileToRename: undefined, - fullDisplayName: '"/b/bin/b".fnB', - kind: ScriptElementKind.functionElement, - kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","), - triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnB"), - }, - locs: [ - { - file: bDts.path, - locs: [protocolRenameSpanFromSubstring(bDts.content, "fnB")], - }, - { - file: userTs.path, - locs: [protocolRenameSpanFromSubstring(userTs.content, "fnB")], - }, - ], - }); - verifySingleInferredProject(session); - }); - - it("getEditsForFileRename", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.GetEditsForFileRename, { - oldFilePath: aTs.path, - newFilePath: "/a/aNew.ts", - }); - assert.deepEqual>(response, [ - { - fileName: userTs.path, - textChanges: [ - { ...protocolTextSpanFromSubstring(userTs.content, "../a/bin/a"), newText: "../a/bin/aNew" }, - ], - }, - ]); - verifySingleInferredProject(session); - }); - - it("getEditsForFileRename when referencing project doesnt include file and its renamed", () => { - const aTs: File = { path: "/a/src/a.ts", content: "" }; - const aTsconfig: File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - composite: true, - declaration: true, - declarationMap: true, - outDir: "./build", - } - }), - }; - const bTs: File = { path: "/b/src/b.ts", content: "" }; - const bTsconfig: File = { - path: "/b/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - composite: true, - outDir: "./build", - }, - include: ["./src"], - references: [{ path: "../a" }], - }), - }; - - const host = createServerHost([aTs, aTsconfig, bTs, bTsconfig]); - const session = createSession(host); - openFilesForSession([aTs, bTs], session); - const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { - oldFilePath: aTs.path, - newFilePath: "/a/src/a1.ts", - }); - assert.deepEqual>(response, []); // Should not change anything - }); - }); - - describe("tsserverProjectSystem with tsbuild projects", () => { - function createHost(files: ReadonlyArray, rootNames: ReadonlyArray) { - const host = createServerHost(files); - - // ts build should succeed - const solutionBuilder = tscWatch.createSolutionBuilder(host, rootNames, {}); - solutionBuilder.buildAllProjects(); - assert.equal(host.getOutput().length, 0); - - return host; - } - - describe("with container project", () => { - function getProjectFiles(project: string): [File, File] { - return [ - TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"), - TestFSWithWatch.getTsBuildProjectFile(project, "index.ts"), - ]; - } - - const project = "container"; - const containerLib = getProjectFiles("container/lib"); - const containerExec = getProjectFiles("container/exec"); - const containerCompositeExec = getProjectFiles("container/compositeExec"); - const containerConfig = TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"); - const files = [libFile, ...containerLib, ...containerExec, ...containerCompositeExec, containerConfig]; - - it("does not error on container only project", () => { - const host = createHost(files, [containerConfig.path]); - - // Open external project for the folder - const session = createSession(host); - const service = session.getProjectService(); - service.openExternalProjects([{ - projectFileName: TestFSWithWatch.getTsBuildProjectFilePath(project, project), - rootFiles: files.map(f => ({ fileName: f.path })), - options: {} - }]); - checkNumberOfProjects(service, { configuredProjects: 4 }); - files.forEach(f => { - const args: protocol.FileRequestArgs = { - file: f.path, - projectFileName: endsWith(f.path, "tsconfig.json") ? f.path : undefined - }; - const syntaxDiagnostics = session.executeCommandSeq({ - command: protocol.CommandTypes.SyntacticDiagnosticsSync, - arguments: args - }).response; - assert.deepEqual(syntaxDiagnostics, []); - const semanticDiagnostics = session.executeCommandSeq({ - command: protocol.CommandTypes.SemanticDiagnosticsSync, - arguments: args - }).response; - assert.deepEqual(semanticDiagnostics, []); - }); - const containerProject = service.configuredProjects.get(containerConfig.path)!; - checkProjectActualFiles(containerProject, [containerConfig.path]); - const optionsDiagnostics = session.executeCommandSeq({ - command: protocol.CommandTypes.CompilerOptionsDiagnosticsFull, - arguments: { projectFileName: containerProject.projectName } - }).response; - assert.deepEqual(optionsDiagnostics, []); - }); - - it("can successfully find references with --out options", () => { - const host = createHost(files, [containerConfig.path]); - const session = createSession(host); - openFilesForSession([containerCompositeExec[1]], session); - const service = session.getProjectService(); - checkNumberOfProjects(service, { configuredProjects: 1 }); - const locationOfMyConst = protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst"); - const response = session.executeCommandSeq({ - command: protocol.CommandTypes.Rename, - arguments: { - file: containerCompositeExec[1].path, - ...locationOfMyConst - } - }).response as protocol.RenameResponseBody; - - - const myConstLen = "myConst".length; - const locationOfMyConstInLib = protocolLocationFromSubstring(containerLib[1].content, "myConst"); - assert.deepEqual(response.locs, [ - { file: containerCompositeExec[1].path, locs: [{ start: locationOfMyConst, end: { line: locationOfMyConst.line, offset: locationOfMyConst.offset + myConstLen } }] }, - { file: containerLib[1].path, locs: [{ start: locationOfMyConstInLib, end: { line: locationOfMyConstInLib.line, offset: locationOfMyConstInLib.offset + myConstLen } }] } - ]); - }); - }); - - describe("with main and depedency project", () => { - const projectLocation = "/user/username/projects/myproject"; - const dependecyLocation = `${projectLocation}/dependency`; - const mainLocation = `${projectLocation}/main`; - const dependencyTs: File = { - path: `${dependecyLocation}/FnS.ts`, - content: `export function fn1() { } -export function fn2() { } -export function fn3() { } -export function fn4() { } -export function fn5() { } -` - }; - const dependencyConfig: File = { - path: `${dependecyLocation}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true } }) - }; - - const mainTs: File = { - path: `${mainLocation}/main.ts`, - content: `import { - fn1, - fn2, - fn3, - fn4, - fn5 -} from '../dependency/fns' - -fn1(); -fn2(); -fn3(); -fn4(); -fn5(); -` - }; - const mainConfig: File = { - path: `${mainLocation}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true, declarationMap: true }, - references: [{ path: "../dependency" }] - }) - }; - - const randomFile: File = { - path: `${projectLocation}/random/random.ts`, - content: "let a = 10;" - }; - const randomConfig: File = { - path: `${projectLocation}/random/tsconfig.json`, - content: "{}" - }; - const dtsLocation = `${dependecyLocation}/FnS.d.ts`; - const dtsPath = dtsLocation.toLowerCase() as Path; - const dtsMapLocation = `${dtsLocation}.map`; - const dtsMapPath = dtsMapLocation.toLowerCase() as Path; - - const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile, randomFile, randomConfig]; - - function verifyScriptInfos(session: TestSession, host: TestServerHost, openInfos: ReadonlyArray, closedInfos: ReadonlyArray, otherWatchedFiles: ReadonlyArray) { - checkScriptInfos(session.getProjectService(), openInfos.concat(closedInfos)); - checkWatchedFiles(host, closedInfos.concat(otherWatchedFiles).map(f => f.toLowerCase())); - } - - function verifyInfosWithRandom(session: TestSession, host: TestServerHost, openInfos: ReadonlyArray, closedInfos: ReadonlyArray, otherWatchedFiles: ReadonlyArray) { - verifyScriptInfos(session, host, openInfos.concat(randomFile.path), closedInfos, otherWatchedFiles.concat(randomConfig.path)); - } - - function verifyOnlyRandomInfos(session: TestSession, host: TestServerHost) { - verifyScriptInfos(session, host, [randomFile.path], [libFile.path], [randomConfig.path]); - } - - // Returns request and expected Response, expected response when no map file - interface SessionAction { - reqName: string; - request: Partial; - expectedResponse: Response; - expectedResponseNoMap?: Response; - expectedResponseNoDts?: Response; - } - function gotoDefintinionFromMainTs(fn: number): SessionAction { - const textSpan = usageSpan(fn); - const definition: protocol.FileSpan = { file: dependencyTs.path, ...definitionSpan(fn) }; - const declareSpaceLength = "declare ".length; - return { - reqName: "goToDef", - request: { - command: protocol.CommandTypes.DefinitionAndBoundSpan, - arguments: { file: mainTs.path, ...textSpan.start } - }, - expectedResponse: { - // To dependency - definitions: [definition], - textSpan - }, - expectedResponseNoMap: { - // To the dts - definitions: [{ file: dtsPath, start: { line: fn, offset: definition.start.offset + declareSpaceLength }, end: { line: fn, offset: definition.end.offset + declareSpaceLength } }], - textSpan - }, - expectedResponseNoDts: { - // To import declaration - definitions: [{ file: mainTs.path, ...importSpan(fn) }], - textSpan - } - }; - } - - function definitionSpan(fn: number): protocol.TextSpan { - return { start: { line: fn, offset: 17 }, end: { line: fn, offset: 20 } }; - } - function importSpan(fn: number): protocol.TextSpan { - return { start: { line: fn + 1, offset: 5 }, end: { line: fn + 1, offset: 8 } }; - } - function usageSpan(fn: number): protocol.TextSpan { - return { start: { line: fn + 8, offset: 1 }, end: { line: fn + 8, offset: 4 } }; - } - - function renameFromDependencyTs(fn: number): SessionAction { - const triggerSpan = definitionSpan(fn); - return { - reqName: "rename", - request: { - command: protocol.CommandTypes.Rename, - arguments: { file: dependencyTs.path, ...triggerSpan.start } - }, - expectedResponse: { - info: { - canRename: true, - fileToRename: undefined, - displayName: `fn${fn}`, - fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`, - kind: ScriptElementKind.functionElement, - kindModifiers: "export", - triggerSpan - }, - locs: [ - { file: dependencyTs.path, locs: [triggerSpan] } - ] - } - }; - } - - function renameFromDependencyTsWithBothProjectsOpen(fn: number): SessionAction { - const { reqName, request, expectedResponse } = renameFromDependencyTs(fn); - const { info, locs } = expectedResponse; - return { - reqName, - request, - expectedResponse: { - info, - locs: [ - locs[0], - { - file: mainTs.path, - locs: [ - importSpan(fn), - usageSpan(fn) - ] - } - ] - }, - // Only dependency result - expectedResponseNoMap: expectedResponse, - expectedResponseNoDts: expectedResponse - }; - } - - // Returns request and expected Response - type SessionActionGetter = (fn: number) => SessionAction; - // Open File, expectedProjectActualFiles, actionGetter, openFileLastLine - interface DocumentPositionMapperVerifier { - openFile: File; - expectedProjectActualFiles: ReadonlyArray; - actionGetter: SessionActionGetter; - openFileLastLine: number; - } - function verifyDocumentPositionMapperUpdates( - mainScenario: string, - verifier: ReadonlyArray, - closedInfos: ReadonlyArray) { - - const openFiles = verifier.map(v => v.openFile); - const expectedProjectActualFiles = verifier.map(v => v.expectedProjectActualFiles); - const actionGetters = verifier.map(v => v.actionGetter); - const openFileLastLines = verifier.map(v => v.openFileLastLine); - - const configFiles = openFiles.map(openFile => `${getDirectoryPath(openFile.path)}/tsconfig.json`); - const openInfos = openFiles.map(f => f.path); - // When usage and dependency are used, dependency config is part of closedInfo so ignore - const otherWatchedFiles = verifier.length > 1 ? [configFiles[0]] : configFiles; - function openTsFile(onHostCreate?: (host: TestServerHost) => void) { - const host = createHost(files, [mainConfig.path]); - if (onHostCreate) { - onHostCreate(host); - } - const session = createSession(host); - openFilesForSession([...openFiles, randomFile], session); - return { host, session }; - } - - function checkProject(session: TestSession, noDts?: true) { - const service = session.getProjectService(); - checkNumberOfProjects(service, { configuredProjects: 1 + verifier.length }); - configFiles.forEach((configFile, index) => { - checkProjectActualFiles( - service.configuredProjects.get(configFile)!, - noDts ? - expectedProjectActualFiles[index].filter(f => f.toLowerCase() !== dtsPath) : - expectedProjectActualFiles[index] - ); - }); - } - - function verifyInfos(session: TestSession, host: TestServerHost) { - verifyInfosWithRandom(session, host, openInfos, closedInfos, otherWatchedFiles); - } - - function verifyInfosWhenNoMapFile(session: TestSession, host: TestServerHost, dependencyTsOK?: true) { - const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined); - verifyInfosWithRandom( - session, - host, - openInfos, - closedInfos.filter(f => f !== dtsMapClosedInfo && (dependencyTsOK || f !== dependencyTs.path)), - dtsMapClosedInfo ? otherWatchedFiles.concat(dtsMapClosedInfo) : otherWatchedFiles - ); - } - - function verifyInfosWhenNoDtsFile(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) { - const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined); - const dtsClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsPath ? f : undefined); - verifyInfosWithRandom( - session, - host, - openInfos, - closedInfos.filter(f => (dependencyTsAndMapOk || f !== dtsMapClosedInfo) && f !== dtsClosedInfo && (dependencyTsAndMapOk || f !== dependencyTs.path)), - // When project actual file contains dts, it needs to be watched - dtsClosedInfo && expectedProjectActualFiles.some(expectedProjectActualFiles => expectedProjectActualFiles.some(f => f.toLowerCase() === dtsPath)) ? - otherWatchedFiles.concat(dtsClosedInfo) : - otherWatchedFiles - ); - } - - function verifyDocumentPositionMapper(session: TestSession, dependencyMap: server.ScriptInfo, documentPositionMapper: server.ScriptInfo["documentPositionMapper"], notEqual?: true) { - assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap); - if (notEqual) { - assert.notStrictEqual(dependencyMap.documentPositionMapper, documentPositionMapper); - } - else { - assert.strictEqual(dependencyMap.documentPositionMapper, documentPositionMapper); - } - } - - function action(actionGetter: SessionActionGetter, fn: number, session: TestSession) { - const { reqName, request, expectedResponse, expectedResponseNoMap, expectedResponseNoDts } = actionGetter(fn); - const { response } = session.executeCommandSeq(request); - return { reqName, response, expectedResponse, expectedResponseNoMap, expectedResponseNoDts }; - } - - function firstAction(session: TestSession) { - actionGetters.forEach(actionGetter => action(actionGetter, 1, session)); - } - - function verifyAllFnActionWorker(session: TestSession, verifyAction: (result: ReturnType, dtsInfo: server.ScriptInfo | undefined, isFirst: boolean) => void, dtsAbsent?: true) { - // action - let isFirst = true; - for (const actionGetter of actionGetters) { - for (let fn = 1; fn <= 5; fn++) { - const result = action(actionGetter, fn, session); - const dtsInfo = session.getProjectService().filenameToScriptInfo.get(dtsPath); - if (dtsAbsent) { - assert.isUndefined(dtsInfo); - } - else { - assert.isDefined(dtsInfo); - } - verifyAction(result, dtsInfo, isFirst); - isFirst = false; - } - } - } - - function verifyAllFnAction( - session: TestSession, - host: TestServerHost, - firstDocumentPositionMapperNotEquals?: true, - dependencyMap?: server.ScriptInfo, - documentPositionMapper?: server.ScriptInfo["documentPositionMapper"] - ) { - // action - verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse }, dtsInfo, isFirst) => { - assert.deepEqual(response, expectedResponse, `Failed on ${reqName}`); - verifyInfos(session, host); - assert.equal(dtsInfo!.sourceMapFilePath, dtsMapPath); - if (isFirst) { - if (dependencyMap) { - verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper, firstDocumentPositionMapperNotEquals); - documentPositionMapper = dependencyMap.documentPositionMapper; - } - else { - dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!; - documentPositionMapper = dependencyMap.documentPositionMapper; - } - } - else { - verifyDocumentPositionMapper(session, dependencyMap!, documentPositionMapper); - } - }); - return { dependencyMap: dependencyMap!, documentPositionMapper }; - } - - function verifyAllFnActionWithNoMap( - session: TestSession, - host: TestServerHost, - dependencyTsOK?: true - ) { - let sourceMapFilePath: server.ScriptInfo["sourceMapFilePath"]; - // action - verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoMap }, dtsInfo, isFirst) => { - assert.deepEqual(response, expectedResponseNoMap || expectedResponse, `Failed on ${reqName}`); - verifyInfosWhenNoMapFile(session, host, dependencyTsOK); - assert.isUndefined(session.getProjectService().filenameToScriptInfo.get(dtsMapPath)); - if (isFirst) { - assert.isNotString(dtsInfo!.sourceMapFilePath); - assert.isNotFalse(dtsInfo!.sourceMapFilePath); - assert.isDefined(dtsInfo!.sourceMapFilePath); - sourceMapFilePath = dtsInfo!.sourceMapFilePath; - } - else { - assert.equal(dtsInfo!.sourceMapFilePath, sourceMapFilePath); - } - }); - return sourceMapFilePath; - } - - function verifyAllFnActionWithNoDts( - session: TestSession, - host: TestServerHost, - dependencyTsAndMapOk?: true - ) { - // action - verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoDts }) => { - assert.deepEqual(response, expectedResponseNoDts || expectedResponse, `Failed on ${reqName}`); - verifyInfosWhenNoDtsFile(session, host, dependencyTsAndMapOk); - }, /*dtsAbsent*/ true); - } - - function verifyScenarioWithChangesWorker( - change: (host: TestServerHost, session: TestSession) => void, - afterActionDocumentPositionMapperNotEquals: true | undefined, - timeoutBeforeAction: boolean - ) { - const { host, session } = openTsFile(); - - // Create DocumentPositionMapper - firstAction(session); - const dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!; - const documentPositionMapper = dependencyMap.documentPositionMapper; - - // change - change(host, session); - if (timeoutBeforeAction) { - host.runQueuedTimeoutCallbacks(); - checkProject(session); - verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper); - } - - // action - verifyAllFnAction(session, host, afterActionDocumentPositionMapperNotEquals, dependencyMap, documentPositionMapper); - } - - function verifyScenarioWithChanges( - scenarioName: string, - change: (host: TestServerHost, session: TestSession) => void, - afterActionDocumentPositionMapperNotEquals?: true - ) { - describe(scenarioName, () => { - it("when timeout occurs before request", () => { - verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ true); - }); - - it("when timeout does not occur before request", () => { - verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ false); - }); - }); - } - - function verifyMainScenarioAndScriptInfoCollection(session: TestSession, host: TestServerHost) { - // Main scenario action - const { dependencyMap, documentPositionMapper } = verifyAllFnAction(session, host); - checkProject(session); - verifyInfos(session, host); - - // Collecting at this point retains dependency.d.ts and map - closeFilesForSession([randomFile], session); - openFilesForSession([randomFile], session); - verifyInfos(session, host); - verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper); - - // Closing open file, removes dependencies too - closeFilesForSession([...openFiles, randomFile], session); - openFilesForSession([randomFile], session); - verifyOnlyRandomInfos(session, host); - } - - function verifyMainScenarioAndScriptInfoCollectionWithNoMap(session: TestSession, host: TestServerHost, dependencyTsOKInScenario?: true) { - // Main scenario action - verifyAllFnActionWithNoMap(session, host, dependencyTsOKInScenario); - - // Collecting at this point retains dependency.d.ts and map watcher - closeFilesForSession([randomFile], session); - openFilesForSession([randomFile], session); - verifyInfosWhenNoMapFile(session, host); - - // Closing open file, removes dependencies too - closeFilesForSession([...openFiles, randomFile], session); - openFilesForSession([randomFile], session); - verifyOnlyRandomInfos(session, host); - } - - function verifyMainScenarioAndScriptInfoCollectionWithNoDts(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) { - // Main scenario action - verifyAllFnActionWithNoDts(session, host, dependencyTsAndMapOk); - - // Collecting at this point retains dependency.d.ts and map watcher - closeFilesForSession([randomFile], session); - openFilesForSession([randomFile], session); - verifyInfosWhenNoDtsFile(session, host); - - // Closing open file, removes dependencies too - closeFilesForSession([...openFiles, randomFile], session); - openFilesForSession([randomFile], session); - verifyOnlyRandomInfos(session, host); - } - - function verifyScenarioWhenFileNotPresent( - scenarioName: string, - fileLocation: string, - verifyScenarioAndScriptInfoCollection: (session: TestSession, host: TestServerHost, dependencyTsOk?: true) => void, - noDts?: true - ) { - describe(scenarioName, () => { - it(mainScenario, () => { - const { host, session } = openTsFile(host => host.deleteFile(fileLocation)); - checkProject(session, noDts); - - verifyScenarioAndScriptInfoCollection(session, host); - }); - - it("when file is created", () => { - let fileContents: string | undefined; - const { host, session } = openTsFile(host => { - fileContents = host.readFile(fileLocation); - host.deleteFile(fileLocation); - }); - firstAction(session); - - host.writeFile(fileLocation, fileContents!); - verifyMainScenarioAndScriptInfoCollection(session, host); - }); - - it("when file is deleted", () => { - const { host, session } = openTsFile(); - firstAction(session); - - // The dependency file is deleted when orphan files are collected - host.deleteFile(fileLocation); - verifyScenarioAndScriptInfoCollection(session, host, /*dependencyTsOk*/ true); - }); - }); - } - - it(mainScenario, () => { - const { host, session } = openTsFile(); - checkProject(session); - - verifyMainScenarioAndScriptInfoCollection(session, host); - }); - - // Edit - verifyScenarioWithChanges( - "when usage file changes, document position mapper doesnt change", - (_host, session) => openFiles.forEach( - (openFile, index) => session.executeCommandSeq({ - command: protocol.CommandTypes.Change, - arguments: { file: openFile.path, line: openFileLastLines[index], offset: 1, endLine: openFileLastLines[index], endOffset: 1, insertString: "const x = 10;" } - }) - ) - ); - - // Edit dts to add new fn - verifyScenarioWithChanges( - "when dependency .d.ts changes, document position mapper doesnt change", - host => host.writeFile( - dtsLocation, - host.readFile(dtsLocation)!.replace( - "//# sourceMappingURL=FnS.d.ts.map", - `export declare function fn6(): void; -//# sourceMappingURL=FnS.d.ts.map` - ) - ) - ); - - // Edit map file to represent added new line - verifyScenarioWithChanges( - "when dependency file's map changes", - host => host.writeFile( - dtsMapLocation, - `{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}` - ), - /*afterActionDocumentPositionMapperNotEquals*/ true - ); - - verifyScenarioWhenFileNotPresent( - "when map file is not present", - dtsMapLocation, - verifyMainScenarioAndScriptInfoCollectionWithNoMap - ); - - verifyScenarioWhenFileNotPresent( - "when .d.ts file is not present", - dtsLocation, - verifyMainScenarioAndScriptInfoCollectionWithNoDts, - /*noDts*/ true - ); - } - - const usageVerifier: DocumentPositionMapperVerifier = { - openFile: mainTs, - expectedProjectActualFiles: [mainTs.path, libFile.path, mainConfig.path, dtsPath], - actionGetter: gotoDefintinionFromMainTs, - openFileLastLine: 14 - }; - describe("from project that uses dependency", () => { - const closedInfos = [dependencyTs.path, dependencyConfig.path, libFile.path, dtsPath, dtsMapLocation]; - verifyDocumentPositionMapperUpdates( - "can go to definition correctly", - [usageVerifier], - closedInfos - ); - }); - - const definingVerifier: DocumentPositionMapperVerifier = { - openFile: dependencyTs, - expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path], - actionGetter: renameFromDependencyTs, - openFileLastLine: 6 - }; - describe("from defining project", () => { - const closedInfos = [libFile.path, dtsLocation, dtsMapLocation]; - verifyDocumentPositionMapperUpdates( - "rename locations from dependency", - [definingVerifier], - closedInfos - ); - }); - - describe("when opening depedency and usage project", () => { - const closedInfos = [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path]; - verifyDocumentPositionMapperUpdates( - "goto Definition in usage and rename locations from defining project", - [usageVerifier, { ...definingVerifier, actionGetter: renameFromDependencyTsWithBothProjectsOpen }], - closedInfos - ); - }); - }); - }); - - describe("tsserverProjectSystem duplicate packages", () => { - // Tests that 'moduleSpecifiers.ts' will import from the redirecting file, and not from the file it redirects to, if that can provide a global module specifier. - it("works with import fixes", () => { - const packageContent = "export const foo: number;"; - const packageJsonContent = JSON.stringify({ name: "foo", version: "1.2.3" }); - const aFooIndex: File = { path: "/a/node_modules/foo/index.d.ts", content: packageContent }; - const aFooPackage: File = { path: "/a/node_modules/foo/package.json", content: packageJsonContent }; - const bFooIndex: File = { path: "/b/node_modules/foo/index.d.ts", content: packageContent }; - const bFooPackage: File = { path: "/b/node_modules/foo/package.json", content: packageJsonContent }; - - const userContent = 'import("foo");\nfoo'; - const aUser: File = { path: "/a/user.ts", content: userContent }; - const bUser: File = { path: "/b/user.ts", content: userContent }; - const tsconfig: File = { - path: "/tsconfig.json", - content: "{}", - }; - - const host = createServerHost([aFooIndex, aFooPackage, bFooIndex, bFooPackage, aUser, bUser, tsconfig]); - const session = createSession(host); - - openFilesForSession([aUser, bUser], session); - - for (const user of [aUser, bUser]) { - const response = executeSessionRequest(session, protocol.CommandTypes.GetCodeFixes, { - file: user.path, - startLine: 2, - startOffset: 1, - endLine: 2, - endOffset: 4, - errorCodes: [Diagnostics.Cannot_find_name_0.code], - }); - assert.deepEqual | undefined>(response, [ - { - description: `Import 'foo' from module "foo"`, - fixName: "import", - fixId: "fixMissingImport", - fixAllDescription: "Add all missing imports", - changes: [{ - fileName: user.path, - textChanges: [{ - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: 'import { foo } from "foo";\n\n', - }], - }], - commands: undefined, - }, - ]); - } - }); - }); - - describe("tsserverProjectSystem Untitled files", () => { - it("Can convert positions to locations", () => { - const aTs: File = { path: "/proj/a.ts", content: "" }; - const tsconfig: File = { path: "/proj/tsconfig.json", content: "{}" }; - const session = createSession(createServerHost([aTs, tsconfig])); - - openFilesForSession([aTs], session); - - const untitledFile = "untitled:^Untitled-1"; - executeSessionRequestNoResponse(session, protocol.CommandTypes.Open, { - file: untitledFile, - fileContent: `/// \nlet foo = 1;\nfooo/**/`, - scriptKindName: "TS", - projectRootPath: "/proj", - }); - - const response = executeSessionRequest(session, protocol.CommandTypes.GetCodeFixes, { - file: untitledFile, - startLine: 3, - startOffset: 1, - endLine: 3, - endOffset: 5, - errorCodes: [Diagnostics.Cannot_find_name_0_Did_you_mean_1.code], - }); - assert.deepEqual | undefined>(response, [ - { - description: "Change spelling to 'foo'", - fixAllDescription: "Fix all detected spelling errors", - fixId: "fixSpelling", - fixName: "spelling", - changes: [{ - fileName: untitledFile, - textChanges: [{ - start: { line: 3, offset: 1 }, - end: { line: 3, offset: 5 }, - newText: "foo", - }], - }], - commands: undefined, - }, - ]); - }); - }); - - describe("tsserverProjectSystem with metadata in response", () => { - const metadata = "Extra Info"; - function verifyOutput(host: TestServerHost, expectedResponse: protocol.Response) { - const output = host.getOutput().map(mapOutputToJson); - assert.deepEqual(output, [expectedResponse]); - host.clearOutput(); - } - - function verifyCommandWithMetadata(session: TestSession, host: TestServerHost, command: Partial, expectedResponseBody: U) { - command.seq = session.getSeq(); - command.type = "request"; - session.onMessage(JSON.stringify(command)); - verifyOutput(host, expectedResponseBody ? - { seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: true, body: expectedResponseBody, metadata } : - { seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: false, message: "No content available." } - ); - } - - const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; - const tsconfig: File = { - path: "/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { plugins: [{ name: "myplugin" }] } - }) - }; - function createHostWithPlugin(files: ReadonlyArray) { - const host = createServerHost(files); - host.require = (_initialPath, moduleName) => { - assert.equal(moduleName, "myplugin"); - return { - module: () => ({ - create(info: server.PluginCreateInfo) { - const proxy = Harness.LanguageService.makeDefaultProxy(info); - proxy.getCompletionsAtPosition = (filename, position, options) => { - const result = info.languageService.getCompletionsAtPosition(filename, position, options); - if (result) { - result.metadata = metadata; - } - return result; - }; - return proxy; - } - }), - error: undefined - }; - }; - return host; - } - - describe("With completion requests", () => { - const completionRequestArgs: protocol.CompletionsRequestArgs = { - file: aTs.path, - line: 1, - offset: aTs.content.indexOf("this.") + 1 + "this.".length - }; - const expectedCompletionEntries: ReadonlyArray = [ - { name: "foo", kind: ScriptElementKind.memberFunctionElement, kindModifiers: "", sortText: "0" }, - { name: "prop", kind: ScriptElementKind.memberVariableElement, kindModifiers: "", sortText: "0" } - ]; - - it("can pass through metadata when the command returns array", () => { - const host = createHostWithPlugin([aTs, tsconfig]); - const session = createSession(host); - openFilesForSession([aTs], session); - verifyCommandWithMetadata>(session, host, { - command: protocol.CommandTypes.Completions, - arguments: completionRequestArgs - }, expectedCompletionEntries); - }); - - it("can pass through metadata when the command returns object", () => { - const host = createHostWithPlugin([aTs, tsconfig]); - const session = createSession(host); - openFilesForSession([aTs], session); - verifyCommandWithMetadata(session, host, { - command: protocol.CommandTypes.CompletionInfo, - arguments: completionRequestArgs - }, { - isGlobalCompletion: false, - isMemberCompletion: true, - isNewIdentifierLocation: false, - entries: expectedCompletionEntries - }); - }); - - it("returns undefined correctly", () => { - const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { const x = 0; } }` }; - const host = createHostWithPlugin([aTs, tsconfig]); - const session = createSession(host); - openFilesForSession([aTs], session); - verifyCommandWithMetadata(session, host, { - command: protocol.CommandTypes.Completions, - arguments: { file: aTs.path, line: 1, offset: aTs.content.indexOf("x") + 1 } - }, /*expectedResponseBody*/ undefined); - }); - }); - }); - - function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem { - return { - ...protocolFileSpanFromSubstring(file, text, options), - isDefinition, - isWriteAccess: isDefinition, - lineText, - }; - } - - function makeReferenceEntry(file: File, isDefinition: boolean, text: string, options?: SpanFromSubstringOptions): ReferenceEntry { - return { - ...documentSpanFromSubstring(file, text, options), - isDefinition, - isWriteAccess: isDefinition, - isInString: undefined, - }; - } - - function checkDeclarationFiles(file: File, session: TestSession, expectedFiles: ReadonlyArray): void { - openFilesForSession([file], session); - const project = Debug.assertDefined(session.getProjectService().getDefaultProjectForFile(file.path as server.NormalizedPath, /*ensureProject*/ false)); - const program = project.getCurrentProgram()!; - const output = getFileEmitOutput(program, Debug.assertDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true); - closeFilesForSession([file], session); - - Debug.assert(!output.emitSkipped); - assert.deepEqual(output.outputFiles, expectedFiles.map((e): OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false }))); - } } From db4d9b3050fec94f4c2687fc223ada8e24b2ab5f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 20 Dec 2018 13:20:53 -0800 Subject: [PATCH 18/20] Add unittests:: on all unittests describe blocks for easy run --- src/testRunner/unittests/asserts.ts | 2 +- src/testRunner/unittests/base64.ts | 2 +- src/testRunner/unittests/builder.ts | 2 +- src/testRunner/unittests/compilerCore.ts | 2 +- .../unittests/config/commandLineParsing.ts | 4 +-- .../config/configurationExtension.ts | 2 +- .../config/convertCompilerOptionsFromJson.ts | 2 +- .../config/convertTypeAcquisitionFromJson.ts | 2 +- .../unittests/config/initializeTSConfig.ts | 2 +- src/testRunner/unittests/config/matchFiles.ts | 2 +- .../unittests/config/projectReferences.ts | 12 ++++---- src/testRunner/unittests/config/showConfig.ts | 2 +- .../unittests/config/tsconfigParsing.ts | 2 +- src/testRunner/unittests/convertToBase64.ts | 2 +- src/testRunner/unittests/customTransforms.ts | 4 +-- .../unittests/evaluation/asyncArrow.ts | 2 +- .../unittests/evaluation/asyncGenerator.ts | 2 +- .../unittests/evaluation/forAwaitOf.ts | 2 +- src/testRunner/unittests/factory.ts | 2 +- src/testRunner/unittests/incrementalParser.ts | 2 +- src/testRunner/unittests/jsDocParsing.ts | 2 +- src/testRunner/unittests/moduleResolution.ts | 14 ++++----- src/testRunner/unittests/parsePseudoBigInt.ts | 4 +-- src/testRunner/unittests/paths.ts | 4 +-- src/testRunner/unittests/printer.ts | 2 +- src/testRunner/unittests/programApi.ts | 4 +-- .../unittests/reuseProgramStructure.ts | 6 ++-- src/testRunner/unittests/semver.ts | 4 +-- .../cancellableLanguageServiceOperations.ts | 2 +- .../unittests/services/colorization.ts | 2 +- .../services/convertToAsyncFunction.ts | 2 +- .../unittests/services/documentRegistry.ts | 2 +- .../unittests/services/extract/constants.ts | 2 +- .../unittests/services/extract/functions.ts | 2 +- .../unittests/services/extract/ranges.ts | 2 +- .../services/extract/symbolWalker.ts | 2 +- .../unittests/services/hostNewLineSupport.ts | 2 +- .../unittests/services/languageService.ts | 2 +- .../unittests/services/organizeImports.ts | 2 +- .../unittests/services/patternMatcher.ts | 2 +- .../unittests/services/preProcessFile.ts | 2 +- .../unittests/services/textChanges.ts | 2 +- .../unittests/services/transpile.ts | 2 +- src/testRunner/unittests/transform.ts | 2 +- src/testRunner/unittests/tsbuild.ts | 30 +++++++++---------- src/testRunner/unittests/tsbuildWatchMode.ts | 2 +- src/testRunner/unittests/tscWatch/emit.ts | 8 ++--- .../unittests/tscWatch/resolutionCache.ts | 4 +-- src/testRunner/unittests/tscWatch/watchApi.ts | 2 +- .../unittests/tscWatch/watchEnvironment.ts | 2 +- .../tsserver/cachingFileSystemInformation.ts | 2 +- .../unittests/tsserver/cancellationToken.ts | 2 +- .../unittests/tsserver/compileOnSave.ts | 4 +-- .../unittests/tsserver/completions.ts | 2 +- .../unittests/tsserver/configFileSearch.ts | 2 +- .../unittests/tsserver/declarationFileMaps.ts | 2 +- .../unittests/tsserver/documentRegistry.ts | 2 +- .../unittests/tsserver/duplicatePackages.ts | 2 +- .../tsserver/events/largeFileReferenced.ts | 2 +- .../tsserver/events/projectLoading.ts | 2 +- .../events/projectUpdatedInBackground.ts | 2 +- .../unittests/tsserver/externalProjects.ts | 2 +- .../forceConsistentCasingInFileNames.ts | 2 +- .../unittests/tsserver/formatSettings.ts | 2 +- .../tsserver/getEditsForFileRename.ts | 2 +- .../unittests/tsserver/importHelpers.ts | 2 +- .../unittests/tsserver/inferredProjects.ts | 2 +- .../unittests/tsserver/languageService.ts | 2 +- .../tsserver/maxNodeModuleJsDepth.ts | 2 +- .../unittests/tsserver/metadataInResponse.ts | 2 +- src/testRunner/unittests/tsserver/navTo.ts | 2 +- .../unittests/tsserver/occurences.ts | 2 +- src/testRunner/unittests/tsserver/openFile.ts | 2 +- .../unittests/tsserver/projectErrors.ts | 12 ++++---- .../unittests/tsserver/projectReferences.ts | 2 +- .../unittests/tsserver/refactors.ts | 2 +- src/testRunner/unittests/tsserver/reload.ts | 2 +- src/testRunner/unittests/tsserver/rename.ts | 2 +- .../unittests/tsserver/resolutionCache.ts | 10 +++---- src/testRunner/unittests/tsserver/session.ts | 10 +++---- .../unittests/tsserver/skipLibCheck.ts | 2 +- src/testRunner/unittests/tsserver/symLinks.ts | 2 +- .../unittests/tsserver/syntaxOperations.ts | 2 +- .../unittests/tsserver/telemetry.ts | 2 +- .../unittests/tsserver/textStorage.ts | 2 +- .../unittests/tsserver/typeAquisition.ts | 4 +-- .../tsserver/typeReferenceDirectives.ts | 2 +- .../unittests/tsserver/typingsInstaller.ts | 20 ++++++------- .../unittests/tsserver/untitledFiles.ts | 2 +- .../unittests/tsserver/versionCache.ts | 6 ++-- .../unittests/tsserver/watchEnvironment.ts | 4 +-- 91 files changed, 155 insertions(+), 155 deletions(-) diff --git a/src/testRunner/unittests/asserts.ts b/src/testRunner/unittests/asserts.ts index 8d5138085a8..c54173e2f90 100644 --- a/src/testRunner/unittests/asserts.ts +++ b/src/testRunner/unittests/asserts.ts @@ -1,5 +1,5 @@ namespace ts { - describe("assert", () => { + describe("unittests:: assert", () => { it("deepEqual", () => { assert.throws(() => assert.deepEqual(createNodeArray([createIdentifier("A")]), createNodeArray([createIdentifier("B")]))); assert.throws(() => assert.deepEqual(createNodeArray([], /*hasTrailingComma*/ true), createNodeArray([], /*hasTrailingComma*/ false))); diff --git a/src/testRunner/unittests/base64.ts b/src/testRunner/unittests/base64.ts index 11c2623df65..1dc51a8fbf5 100644 --- a/src/testRunner/unittests/base64.ts +++ b/src/testRunner/unittests/base64.ts @@ -1,5 +1,5 @@ namespace ts { - describe("base64", () => { + describe("unittests:: base64", () => { describe("base64decode", () => { it("can decode input strings correctly without needing a host implementation", () => { const tests = [ diff --git a/src/testRunner/unittests/builder.ts b/src/testRunner/unittests/builder.ts index 4a163d6b56b..709222a4dff 100644 --- a/src/testRunner/unittests/builder.ts +++ b/src/testRunner/unittests/builder.ts @@ -1,5 +1,5 @@ namespace ts { - describe("builder", () => { + describe("unittests:: builder", () => { it("emits dependent files", () => { const files: NamedSourceText[] = [ { name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") }, diff --git a/src/testRunner/unittests/compilerCore.ts b/src/testRunner/unittests/compilerCore.ts index 27f5cdc0887..4c3918c3149 100644 --- a/src/testRunner/unittests/compilerCore.ts +++ b/src/testRunner/unittests/compilerCore.ts @@ -1,5 +1,5 @@ namespace ts { - describe("compilerCore", () => { + describe("unittests:: compilerCore", () => { describe("equalOwnProperties", () => { it("correctly equates objects", () => { assert.isTrue(equalOwnProperties({}, {})); diff --git a/src/testRunner/unittests/config/commandLineParsing.ts b/src/testRunner/unittests/config/commandLineParsing.ts index be7f6dca3c5..25fa3a45050 100644 --- a/src/testRunner/unittests/config/commandLineParsing.ts +++ b/src/testRunner/unittests/config/commandLineParsing.ts @@ -1,5 +1,5 @@ namespace ts { - describe("config:: commandLineParsing:: parseCommandLine", () => { + describe("unittests:: config:: commandLineParsing:: parseCommandLine", () => { function assertParseResult(commandLine: string[], expectedParsedCommandLine: ParsedCommandLine) { const parsed = parseCommandLine(commandLine); @@ -367,7 +367,7 @@ namespace ts { }); }); - describe("config:: commandLineParsing:: parseBuildOptions", () => { + describe("unittests:: config:: commandLineParsing:: parseBuildOptions", () => { function assertParseResult(commandLine: string[], expectedParsedBuildCommand: ParsedBuildCommand) { const parsed = parseBuildCommand(commandLine); const parsedBuildOptions = JSON.stringify(parsed.buildOptions); diff --git a/src/testRunner/unittests/config/configurationExtension.ts b/src/testRunner/unittests/config/configurationExtension.ts index 0e7cd0821c4..25e975c0fb2 100644 --- a/src/testRunner/unittests/config/configurationExtension.ts +++ b/src/testRunner/unittests/config/configurationExtension.ts @@ -208,7 +208,7 @@ namespace ts { } } - describe("config:: configurationExtension", () => { + describe("unittests:: config:: configurationExtension", () => { forEach<[string, string, fakes.ParseConfigHost], void>([ ["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost], ["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost] diff --git a/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts b/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts index 4a3a42ce216..318bc0ceb26 100644 --- a/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts +++ b/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts @@ -1,5 +1,5 @@ namespace ts { - describe("config:: convertCompilerOptionsFromJson", () => { + describe("unittests:: config:: convertCompilerOptionsFromJson", () => { const formatDiagnosticHost: FormatDiagnosticsHost = { getCurrentDirectory: () => "/apath/", getCanonicalFileName: createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ true), diff --git a/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts index 0998b0b0d7a..890cad4ade6 100644 --- a/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts +++ b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts @@ -1,6 +1,6 @@ namespace ts { interface ExpectedResult { typeAcquisition: TypeAcquisition; errors: Diagnostic[]; } - describe("config:: convertTypeAcquisitionFromJson", () => { + describe("unittests:: config:: convertTypeAcquisitionFromJson", () => { function assertTypeAcquisition(json: any, configFileName: string, expectedResult: ExpectedResult) { assertTypeAcquisitionWithJson(json, configFileName, expectedResult); assertTypeAcquisitionWithJsonNode(json, configFileName, expectedResult); diff --git a/src/testRunner/unittests/config/initializeTSConfig.ts b/src/testRunner/unittests/config/initializeTSConfig.ts index 09d80c7f5c2..f7fb7d2a439 100644 --- a/src/testRunner/unittests/config/initializeTSConfig.ts +++ b/src/testRunner/unittests/config/initializeTSConfig.ts @@ -1,5 +1,5 @@ namespace ts { - describe("config:: initTSConfig", () => { + describe("unittests:: config:: initTSConfig", () => { function initTSConfigCorrectly(name: string, commandLinesArgs: string[]) { describe(name, () => { const commandLine = parseCommandLine(commandLinesArgs); diff --git a/src/testRunner/unittests/config/matchFiles.ts b/src/testRunner/unittests/config/matchFiles.ts index 9155cb26a1c..a30fa468443 100644 --- a/src/testRunner/unittests/config/matchFiles.ts +++ b/src/testRunner/unittests/config/matchFiles.ts @@ -143,7 +143,7 @@ namespace ts { return createFileDiagnostic(file, start, length, diagnosticMessage, arg0); } - describe("config:: matchFiles", () => { + describe("unittests:: config:: matchFiles", () => { it("with defaults", () => { const json = {}; const expected: ParsedCommandLine = { diff --git a/src/testRunner/unittests/config/projectReferences.ts b/src/testRunner/unittests/config/projectReferences.ts index be8e0be6206..266b016c681 100644 --- a/src/testRunner/unittests/config/projectReferences.ts +++ b/src/testRunner/unittests/config/projectReferences.ts @@ -96,7 +96,7 @@ namespace ts { checkResult(prog, host); } - describe("config:: project-references meta check", () => { + describe("unittests:: config:: project-references meta check", () => { it("default setup was created correctly", () => { const spec: TestSpecification = { "/primary": { @@ -118,7 +118,7 @@ namespace ts { /** * Validate that we enforce the basic settings constraints for referenced projects */ - describe("config:: project-references constraint checking for settings", () => { + describe("unittests:: config:: project-references constraint checking for settings", () => { it("errors when declaration = false", () => { const spec: TestSpecification = { "/primary": { @@ -248,7 +248,7 @@ namespace ts { /** * Path mapping behavior */ - describe("config:: project-references path mapping", () => { + describe("unittests:: config:: project-references path mapping", () => { it("redirects to the output .d.ts file", () => { const spec: TestSpecification = { "/alpha": { @@ -268,7 +268,7 @@ namespace ts { }); }); - describe("config:: project-references nice-behavior", () => { + describe("unittests:: config:: project-references nice-behavior", () => { it("issues a nice error when the input file is missing", () => { const spec: TestSpecification = { "/alpha": { @@ -289,7 +289,7 @@ namespace ts { /** * 'composite' behavior */ - describe("config:: project-references behavior changes under composite: true", () => { + describe("unittests:: config:: project-references behavior changes under composite: true", () => { it("doesn't infer the rootDir from source paths", () => { const spec: TestSpecification = { "/alpha": { @@ -308,7 +308,7 @@ namespace ts { }); }); - describe("config:: project-references errors when a file in a composite project occurs outside the root", () => { + describe("unittests:: config:: project-references errors when a file in a composite project occurs outside the root", () => { it("Errors when a file is outside the rootdir", () => { const spec: TestSpecification = { "/alpha": { diff --git a/src/testRunner/unittests/config/showConfig.ts b/src/testRunner/unittests/config/showConfig.ts index f86b37970f5..afe8f878d27 100644 --- a/src/testRunner/unittests/config/showConfig.ts +++ b/src/testRunner/unittests/config/showConfig.ts @@ -1,5 +1,5 @@ namespace ts { - describe("config:: showConfig", () => { + describe("unittests:: config:: showConfig", () => { function showTSConfigCorrectly(name: string, commandLinesArgs: string[], configJson?: object) { describe(name, () => { const outputFileName = `showConfig/${name.replace(/[^a-z0-9\-./ ]/ig, "")}/tsconfig.json`; diff --git a/src/testRunner/unittests/config/tsconfigParsing.ts b/src/testRunner/unittests/config/tsconfigParsing.ts index cc617a0d151..638a394a34e 100644 --- a/src/testRunner/unittests/config/tsconfigParsing.ts +++ b/src/testRunner/unittests/config/tsconfigParsing.ts @@ -1,5 +1,5 @@ namespace ts { - describe("config:: tsconfigParsing:: parseConfigFileTextToJson", () => { + describe("unittests:: config:: tsconfigParsing:: parseConfigFileTextToJson", () => { function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; error?: Diagnostic[] }) { const parsed = parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject)); diff --git a/src/testRunner/unittests/convertToBase64.ts b/src/testRunner/unittests/convertToBase64.ts index 56c95622977..37e38da8da7 100644 --- a/src/testRunner/unittests/convertToBase64.ts +++ b/src/testRunner/unittests/convertToBase64.ts @@ -1,5 +1,5 @@ namespace ts { - describe("convertToBase64", () => { + describe("unittests:: convertToBase64", () => { function runTest(input: string): void { const actual = convertToBase64(input); const expected = sys.base64encode!(input); diff --git a/src/testRunner/unittests/customTransforms.ts b/src/testRunner/unittests/customTransforms.ts index c4ff21d0a25..304c0a55c59 100644 --- a/src/testRunner/unittests/customTransforms.ts +++ b/src/testRunner/unittests/customTransforms.ts @@ -1,5 +1,5 @@ namespace ts { - describe("customTransforms", () => { + describe("unittests:: customTransforms", () => { function emitsCorrectly(name: string, sources: { file: string, text: string }[], customTransformers: CustomTransformers, options: CompilerOptions = {}) { it(name, () => { const roots = sources.map(source => createSourceFile(source.file, source.text, ScriptTarget.ES2015)); @@ -97,4 +97,4 @@ namespace ts { experimentalDecorators: true }); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/evaluation/asyncArrow.ts b/src/testRunner/unittests/evaluation/asyncArrow.ts index 01959ca150f..04a7af613fa 100644 --- a/src/testRunner/unittests/evaluation/asyncArrow.ts +++ b/src/testRunner/unittests/evaluation/asyncArrow.ts @@ -1,4 +1,4 @@ -describe("evaluation:: asyncArrowEvaluation", () => { +describe("unittests:: evaluation:: asyncArrowEvaluation", () => { // https://github.com/Microsoft/TypeScript/issues/24722 it("this capture (es5)", async () => { const result = evaluator.evaluateTypeScript(` diff --git a/src/testRunner/unittests/evaluation/asyncGenerator.ts b/src/testRunner/unittests/evaluation/asyncGenerator.ts index cb630d28724..8ca531f0759 100644 --- a/src/testRunner/unittests/evaluation/asyncGenerator.ts +++ b/src/testRunner/unittests/evaluation/asyncGenerator.ts @@ -1,4 +1,4 @@ -describe("evaluation:: asyncGeneratorEvaluation", () => { +describe("unittests:: evaluation:: asyncGeneratorEvaluation", () => { it("return (es5)", async () => { const result = evaluator.evaluateTypeScript(` async function * g() { diff --git a/src/testRunner/unittests/evaluation/forAwaitOf.ts b/src/testRunner/unittests/evaluation/forAwaitOf.ts index 7e7ee41e2f1..c7be018cbc9 100644 --- a/src/testRunner/unittests/evaluation/forAwaitOf.ts +++ b/src/testRunner/unittests/evaluation/forAwaitOf.ts @@ -1,4 +1,4 @@ -describe("evaluation:: forAwaitOfEvaluation", () => { +describe("unittests:: evaluation:: forAwaitOfEvaluation", () => { it("sync (es5)", async () => { const result = evaluator.evaluateTypeScript(` let i = 0; diff --git a/src/testRunner/unittests/factory.ts b/src/testRunner/unittests/factory.ts index 402c399d728..cc760059c19 100644 --- a/src/testRunner/unittests/factory.ts +++ b/src/testRunner/unittests/factory.ts @@ -1,5 +1,5 @@ namespace ts { - describe("FactoryAPI", () => { + describe("unittests:: FactoryAPI", () => { function assertSyntaxKind(node: Node, expected: SyntaxKind) { assert.strictEqual(node.kind, expected, `Actual: ${Debug.showSyntaxKind(node)} Expected: ${(ts as any).SyntaxKind[expected]}`); } diff --git a/src/testRunner/unittests/incrementalParser.ts b/src/testRunner/unittests/incrementalParser.ts index fb3408b1fc3..8af44494803 100644 --- a/src/testRunner/unittests/incrementalParser.ts +++ b/src/testRunner/unittests/incrementalParser.ts @@ -120,7 +120,7 @@ namespace ts { } } - describe("Incremental", () => { + describe("unittests:: Incremental Parser", () => { it("Inserting into method", () => { const source = "class C {\r\n" + " public foo1() { }\r\n" + diff --git a/src/testRunner/unittests/jsDocParsing.ts b/src/testRunner/unittests/jsDocParsing.ts index 931785aa94d..4171a330f32 100644 --- a/src/testRunner/unittests/jsDocParsing.ts +++ b/src/testRunner/unittests/jsDocParsing.ts @@ -1,5 +1,5 @@ namespace ts { - describe("JSDocParsing", () => { + describe("unittests:: JSDocParsing", () => { describe("TypeExpressions", () => { function parsesCorrectly(name: string, content: string) { it(name, () => { diff --git a/src/testRunner/unittests/moduleResolution.ts b/src/testRunner/unittests/moduleResolution.ts index 28f25437149..69560e5449c 100644 --- a/src/testRunner/unittests/moduleResolution.ts +++ b/src/testRunner/unittests/moduleResolution.ts @@ -80,7 +80,7 @@ namespace ts { } } - describe("moduleResolution:: Node module resolution - relative paths", () => { + describe("unittests:: moduleResolution:: Node module resolution - relative paths", () => { function testLoadAsFile(containingFileName: string, moduleFileNameNoExt: string, moduleName: string): void { for (const ext of supportedTSExtensions) { @@ -200,7 +200,7 @@ namespace ts { }); }); - describe("moduleResolution:: Node module resolution - non-relative paths", () => { + describe("unittests:: moduleResolution:: Node module resolution - non-relative paths", () => { it("computes correct commonPrefix for moduleName cache", () => { const resolutionCache = createModuleResolutionCache("/", (f) => f); let cache = resolutionCache.getOrCreateCacheForModuleName("a"); @@ -457,7 +457,7 @@ namespace ts { }); }); - describe("moduleResolution:: Relative imports", () => { + describe("unittests:: moduleResolution:: Relative imports", () => { function test(files: Map, currentDirectory: string, rootFiles: string[], expectedFilesCount: number, relativeNamesToCheck: string[]) { const options: CompilerOptions = { module: ModuleKind.CommonJS }; const host: CompilerHost = { @@ -530,7 +530,7 @@ export = C; }); }); - describe("moduleResolution:: Files with different casing", () => { + describe("unittests:: moduleResolution:: Files with different casing", () => { let library: SourceFile; function test(files: Map, options: CompilerOptions, currentDirectory: string, useCaseSensitiveFileNames: boolean, rootFiles: string[], diagnosticCodes: number[]): void { const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); @@ -651,7 +651,7 @@ import b = require("./moduleB"); }); }); - describe("moduleResolution:: baseUrl augmented module resolution", () => { + describe("unittests:: moduleResolution:: baseUrl augmented module resolution", () => { it("module resolution without path mappings/rootDirs", () => { test(/*hasDirectoryExists*/ false); @@ -1098,7 +1098,7 @@ import b = require("./moduleB"); }); }); - describe("moduleResolution:: ModuleResolutionHost.directoryExists", () => { + describe("unittests:: moduleResolution:: ModuleResolutionHost.directoryExists", () => { it("No 'fileExists' calls if containing directory is missing", () => { const host: ModuleResolutionHost = { readFile: notImplemented, @@ -1111,7 +1111,7 @@ import b = require("./moduleB"); }); }); - describe("moduleResolution:: Type reference directive resolution: ", () => { + describe("unittests:: moduleResolution:: Type reference directive resolution: ", () => { function testWorker(hasDirectoryExists: boolean, typesRoot: string | undefined, typeDirective: string, primary: boolean, initialFile: File, targetFile: File, ...otherFiles: File[]) { const host = createModuleResolutionHost(hasDirectoryExists, ...[initialFile, targetFile].concat(...otherFiles)); const result = resolveTypeReferenceDirective(typeDirective, initialFile.name, typesRoot ? { typeRoots: [typesRoot] } : {}, host); diff --git a/src/testRunner/unittests/parsePseudoBigInt.ts b/src/testRunner/unittests/parsePseudoBigInt.ts index 0ffbee6345e..db1a841dc2b 100644 --- a/src/testRunner/unittests/parsePseudoBigInt.ts +++ b/src/testRunner/unittests/parsePseudoBigInt.ts @@ -1,5 +1,5 @@ namespace ts { - describe("BigInt literal base conversions", () => { + describe("unittests:: BigInt literal base conversions", () => { describe("parsePseudoBigInt", () => { const testNumbers: number[] = []; for (let i = 0; i < 1e3; i++) testNumbers.push(i); @@ -68,4 +68,4 @@ namespace ts { }); }); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/paths.ts b/src/testRunner/unittests/paths.ts index 0b28bedc929..b1555ab2174 100644 --- a/src/testRunner/unittests/paths.ts +++ b/src/testRunner/unittests/paths.ts @@ -1,4 +1,4 @@ -describe("core paths", () => { +describe("unittests:: core paths", () => { it("normalizeSlashes", () => { assert.strictEqual(ts.normalizeSlashes("a"), "a"); assert.strictEqual(ts.normalizeSlashes("a/b"), "a/b"); @@ -289,4 +289,4 @@ describe("core paths", () => { assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b/c", "file:///a/b", /*ignoreCase*/ false), ".."); assert.strictEqual(ts.getRelativePathFromDirectory("file:///c:", "file:///d:", /*ignoreCase*/ false), "file:///d:/"); }); -}); \ No newline at end of file +}); diff --git a/src/testRunner/unittests/printer.ts b/src/testRunner/unittests/printer.ts index 2e69cf60876..8e217aa6a3a 100644 --- a/src/testRunner/unittests/printer.ts +++ b/src/testRunner/unittests/printer.ts @@ -1,5 +1,5 @@ namespace ts { - describe("PrinterAPI", () => { + describe("unittests:: PrinterAPI", () => { function makePrintsCorrectly(prefix: string) { return function printsCorrectly(name: string, options: PrinterOptions, printCallback: (printer: Printer) => string) { it(name, () => { diff --git a/src/testRunner/unittests/programApi.ts b/src/testRunner/unittests/programApi.ts index 8fa107513fe..01ef6610dba 100644 --- a/src/testRunner/unittests/programApi.ts +++ b/src/testRunner/unittests/programApi.ts @@ -11,7 +11,7 @@ namespace ts { assert.equal(notFound.length, 0, `Not found ${notFound} in actual: ${missingPaths} expected: ${expected}`); } - describe("Program.getMissingFilePaths", () => { + describe("unittests:: Program.getMissingFilePaths", () => { const options: CompilerOptions = { noLib: true, @@ -127,7 +127,7 @@ namespace ts { }); }); - describe("Program.isSourceFileFromExternalLibrary", () => { + describe("unittests:: Program.isSourceFileFromExternalLibrary", () => { it("works on redirect files", () => { // In this example '/node_modules/foo/index.d.ts' will redirect to '/node_modules/bar/node_modules/foo/index.d.ts'. const a = new documents.TextDocument("/a.ts", 'import * as bar from "bar"; import * as foo from "foo";'); diff --git a/src/testRunner/unittests/reuseProgramStructure.ts b/src/testRunner/unittests/reuseProgramStructure.ts index 63b670912d8..c9ea0e77fec 100644 --- a/src/testRunner/unittests/reuseProgramStructure.ts +++ b/src/testRunner/unittests/reuseProgramStructure.ts @@ -210,7 +210,7 @@ namespace ts { checkCache("resolved type directives", program, fileName, expectedContent, f => f.resolvedTypeReferenceDirectiveNames, checkResolvedTypeDirective); } - describe("Reuse program structure:: General", () => { + describe("unittests:: Reuse program structure:: General", () => { const target = ScriptTarget.Latest; const files: NamedSourceText[] = [ { @@ -895,7 +895,7 @@ namespace ts { }); }); - describe("Reuse program structure:: host is optional", () => { + describe("unittests:: Reuse program structure:: host is optional", () => { it("should work if host is not provided", () => { createProgram([], {}); }); @@ -905,7 +905,7 @@ namespace ts { import createTestSystem = TestFSWithWatch.createWatchedSystem; import libFile = TestFSWithWatch.libFile; - describe("Reuse program structure:: isProgramUptoDate should return true when there is no change in compiler options and", () => { + describe("unittests:: Reuse program structure:: isProgramUptoDate should return true when there is no change in compiler options and", () => { function verifyProgramIsUptoDate( program: Program, newRootFileNames: string[], diff --git a/src/testRunner/unittests/semver.ts b/src/testRunner/unittests/semver.ts index 357a24307ca..079bdc3d512 100644 --- a/src/testRunner/unittests/semver.ts +++ b/src/testRunner/unittests/semver.ts @@ -1,6 +1,6 @@ namespace ts { import theory = utils.theory; - describe("semver", () => { + describe("unittests:: semver", () => { describe("Version", () => { function assertVersion(version: Version, [major, minor, patch, prerelease, build]: [number, number, number, string[]?, string[]?]) { assert.strictEqual(version.major, major); @@ -225,4 +225,4 @@ namespace ts { ]); }); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts b/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts index 1d7e12d9825..9868d628d0c 100644 --- a/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts +++ b/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts @@ -1,5 +1,5 @@ namespace ts { - describe("services:: cancellableLanguageServiceOperations", () => { + describe("unittests:: services:: cancellableLanguageServiceOperations", () => { const file = ` function foo(): void; function foo(x: T): T; diff --git a/src/testRunner/unittests/services/colorization.ts b/src/testRunner/unittests/services/colorization.ts index 54ea98dd563..71ba8bad394 100644 --- a/src/testRunner/unittests/services/colorization.ts +++ b/src/testRunner/unittests/services/colorization.ts @@ -6,7 +6,7 @@ interface ClassificationEntry { position?: number; } -describe("services:: Colorization", () => { +describe("unittests:: services:: Colorization", () => { // Use the shim adapter to ensure test coverage of the shim layer for the classifier const languageServiceAdapter = new Harness.LanguageService.ShimLanguageServiceAdapter(/*preprocessToResolve*/ false); const classifier = languageServiceAdapter.getClassifier(); diff --git a/src/testRunner/unittests/services/convertToAsyncFunction.ts b/src/testRunner/unittests/services/convertToAsyncFunction.ts index e8d857f2ee0..b89e684183b 100644 --- a/src/testRunner/unittests/services/convertToAsyncFunction.ts +++ b/src/testRunner/unittests/services/convertToAsyncFunction.ts @@ -343,7 +343,7 @@ interface Array {}` } } - describe("services:: convertToAsyncFunctions", () => { + describe("unittests:: services:: convertToAsyncFunctions", () => { _testConvertToAsyncFunction("convertToAsyncFunction_basic", ` function [#|f|](): Promise{ return fetch('https://typescriptlang.org').then(result => { console.log(result) }); diff --git a/src/testRunner/unittests/services/documentRegistry.ts b/src/testRunner/unittests/services/documentRegistry.ts index 96f2aaff76b..e7c80486e97 100644 --- a/src/testRunner/unittests/services/documentRegistry.ts +++ b/src/testRunner/unittests/services/documentRegistry.ts @@ -1,4 +1,4 @@ -describe("services:: DocumentRegistry", () => { +describe("unittests:: services:: DocumentRegistry", () => { it("documents are shared between projects", () => { const documentRegistry = ts.createDocumentRegistry(); const defaultCompilerOptions = ts.getDefaultCompilerOptions(); diff --git a/src/testRunner/unittests/services/extract/constants.ts b/src/testRunner/unittests/services/extract/constants.ts index 1ca5a719a82..d9cd5010d5b 100644 --- a/src/testRunner/unittests/services/extract/constants.ts +++ b/src/testRunner/unittests/services/extract/constants.ts @@ -1,5 +1,5 @@ namespace ts { - describe("services:: extract:: extractConstants", () => { + describe("unittests:: services:: extract:: extractConstants", () => { testExtractConstant("extractConstant_TopLevel", `let x = [#|1|];`); diff --git a/src/testRunner/unittests/services/extract/functions.ts b/src/testRunner/unittests/services/extract/functions.ts index 21c5e8e9001..d872ad81a63 100644 --- a/src/testRunner/unittests/services/extract/functions.ts +++ b/src/testRunner/unittests/services/extract/functions.ts @@ -1,5 +1,5 @@ namespace ts { - describe("services:: extract:: extractFunctions", () => { + describe("unittests:: services:: extract:: extractFunctions", () => { testExtractFunction("extractFunction1", `namespace A { let x = 1; diff --git a/src/testRunner/unittests/services/extract/ranges.ts b/src/testRunner/unittests/services/extract/ranges.ts index dc62618e0c6..265dd33e60a 100644 --- a/src/testRunner/unittests/services/extract/ranges.ts +++ b/src/testRunner/unittests/services/extract/ranges.ts @@ -42,7 +42,7 @@ namespace ts { } } - describe("services:: extract:: extractRanges", () => { + describe("unittests:: services:: extract:: extractRanges", () => { it("get extract range from selection", () => { testExtractRange(` [#| diff --git a/src/testRunner/unittests/services/extract/symbolWalker.ts b/src/testRunner/unittests/services/extract/symbolWalker.ts index 9d323a7544a..58d9dcb577f 100644 --- a/src/testRunner/unittests/services/extract/symbolWalker.ts +++ b/src/testRunner/unittests/services/extract/symbolWalker.ts @@ -1,5 +1,5 @@ namespace ts { - describe("services:: extract:: Symbol Walker", () => { + describe("unittests:: services:: extract:: Symbol Walker", () => { function test(description: string, source: string, verifier: (file: SourceFile, checker: TypeChecker) => void) { it(description, () => { const result = Harness.Compiler.compileFiles([{ diff --git a/src/testRunner/unittests/services/hostNewLineSupport.ts b/src/testRunner/unittests/services/hostNewLineSupport.ts index 2f00c7b08e5..cafe4813431 100644 --- a/src/testRunner/unittests/services/hostNewLineSupport.ts +++ b/src/testRunner/unittests/services/hostNewLineSupport.ts @@ -1,5 +1,5 @@ namespace ts { - describe("services:: hostNewLineSupport", () => { + describe("unittests:: services:: hostNewLineSupport", () => { function testLSWithFiles(settings: CompilerOptions, files: Harness.Compiler.TestFile[]) { function snapFor(path: string): IScriptSnapshot | undefined { if (path === "lib.d.ts") { diff --git a/src/testRunner/unittests/services/languageService.ts b/src/testRunner/unittests/services/languageService.ts index 332d59ca68a..d646f32383f 100644 --- a/src/testRunner/unittests/services/languageService.ts +++ b/src/testRunner/unittests/services/languageService.ts @@ -1,5 +1,5 @@ namespace ts { - describe("services:: languageService", () => { + describe("unittests:: services:: languageService", () => { const files: {[index: string]: string} = { "foo.ts": `import Vue from "./vue"; import Component from "./vue-class-component"; diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index a45c7548394..1421780a4b4 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -1,5 +1,5 @@ namespace ts { - describe("services:: Organize imports", () => { + describe("unittests:: services:: Organize imports", () => { describe("Sort imports", () => { it("Sort - non-relative vs non-relative", () => { assertSortsBefore( diff --git a/src/testRunner/unittests/services/patternMatcher.ts b/src/testRunner/unittests/services/patternMatcher.ts index 304a9225e0d..9f3cbf4b6d7 100644 --- a/src/testRunner/unittests/services/patternMatcher.ts +++ b/src/testRunner/unittests/services/patternMatcher.ts @@ -1,4 +1,4 @@ -describe("services:: PatternMatcher", () => { +describe("unittests:: services:: PatternMatcher", () => { describe("BreakIntoCharacterSpans", () => { it("EmptyIdentifier", () => { verifyBreakIntoCharacterSpans(""); diff --git a/src/testRunner/unittests/services/preProcessFile.ts b/src/testRunner/unittests/services/preProcessFile.ts index caf412d6403..3e9d16672db 100644 --- a/src/testRunner/unittests/services/preProcessFile.ts +++ b/src/testRunner/unittests/services/preProcessFile.ts @@ -1,4 +1,4 @@ -describe("services:: PreProcessFile:", () => { +describe("unittests:: services:: PreProcessFile:", () => { function test(sourceText: string, readImportFile: boolean, detectJavaScriptImports: boolean, expectedPreProcess: ts.PreProcessedFileInfo): void { const resultPreProcess = ts.preProcessFile(sourceText, readImportFile, detectJavaScriptImports); diff --git a/src/testRunner/unittests/services/textChanges.ts b/src/testRunner/unittests/services/textChanges.ts index fa5e723419a..de92efafbeb 100644 --- a/src/testRunner/unittests/services/textChanges.ts +++ b/src/testRunner/unittests/services/textChanges.ts @@ -2,7 +2,7 @@ // tslint:disable trim-trailing-whitespace namespace ts { - describe("services:: textChanges", () => { + describe("unittests:: services:: textChanges", () => { function findChild(name: string, n: Node) { return find(n)!; diff --git a/src/testRunner/unittests/services/transpile.ts b/src/testRunner/unittests/services/transpile.ts index 533f07884f4..d197d6db621 100644 --- a/src/testRunner/unittests/services/transpile.ts +++ b/src/testRunner/unittests/services/transpile.ts @@ -1,5 +1,5 @@ namespace ts { - describe("services:: Transpile", () => { + describe("unittests:: services:: Transpile", () => { interface TranspileTestSettings { options?: TranspileOptions; diff --git a/src/testRunner/unittests/transform.ts b/src/testRunner/unittests/transform.ts index f219fb6c683..98c29025d5f 100644 --- a/src/testRunner/unittests/transform.ts +++ b/src/testRunner/unittests/transform.ts @@ -1,5 +1,5 @@ namespace ts { - describe("TransformAPI", () => { + describe("unittests:: TransformAPI", () => { function replaceUndefinedWithVoid0(context: TransformationContext) { const previousOnSubstituteNode = context.onSubstituteNode; context.enableSubstitution(SyntaxKind.Identifier); diff --git a/src/testRunner/unittests/tsbuild.ts b/src/testRunner/unittests/tsbuild.ts index 72b1121e861..bf628e10598 100644 --- a/src/testRunner/unittests/tsbuild.ts +++ b/src/testRunner/unittests/tsbuild.ts @@ -8,7 +8,7 @@ namespace ts { "/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map", "/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts"]; - describe("tsbuild - sanity check of clean build of 'sample1' project", () => { + describe("unittests:: tsbuild - sanity check of clean build of 'sample1' project", () => { it("can build the sample project 'sample1' without error", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -61,7 +61,7 @@ namespace ts { }); }); - describe("tsbuild - dry builds", () => { + describe("unittests:: tsbuild - dry builds", () => { it("doesn't write any files in a dry build", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -90,7 +90,7 @@ namespace ts { }); }); - describe("tsbuild - clean builds", () => { + describe("unittests:: tsbuild - clean builds", () => { it("removes all files it built", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -111,7 +111,7 @@ namespace ts { }); }); - describe("tsbuild - force builds", () => { + describe("unittests:: tsbuild - force builds", () => { it("always builds under --force", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -137,7 +137,7 @@ namespace ts { }); }); - describe("tsbuild - can detect when and what to rebuild", () => { + describe("unittests:: tsbuild - can detect when and what to rebuild", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: true }); @@ -200,7 +200,7 @@ namespace ts { }); }); - describe("tsbuild - downstream-blocked compilations", () => { + describe("unittests:: tsbuild - downstream-blocked compilations", () => { it("won't build downstream projects if upstream projects have errors", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -222,7 +222,7 @@ namespace ts { }); }); - describe("tsbuild - project invalidation", () => { + describe("unittests:: tsbuild - project invalidation", () => { it("invalidates projects correctly", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -270,7 +270,7 @@ export class cNew {}`); }); }); - describe("tsbuild - with resolveJsonModule option", () => { + describe("unittests:: tsbuild - with resolveJsonModule option", () => { const projFs = loadProjectFromDisk("tests/projects/resolveJsonModuleAndComposite"); const allExpectedOutputs = ["/src/tests/dist/src/index.js", "/src/tests/dist/src/index.d.ts", "/src/tests/dist/src/hello.json"]; @@ -320,7 +320,7 @@ export default hello.hello`); }); }); - describe("tsbuild - lists files", () => { + describe("unittests:: tsbuild - lists files", () => { it("listFiles", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -369,7 +369,7 @@ export default hello.hello`); }); }); - describe("tsbuild - with rootDir of project reference in parentDirectory", () => { + describe("unittests:: tsbuild - with rootDir of project reference in parentDirectory", () => { const projFs = loadProjectFromDisk("tests/projects/projectReferenceWithRootDirInParent"); const allExpectedOutputs = [ "/src/dist/other/other.js", "/src/dist/other/other.d.ts", @@ -388,7 +388,7 @@ export default hello.hello`); }); }); - describe("tsbuild - when project reference is referenced transitively", () => { + describe("unittests:: tsbuild - when project reference is referenced transitively", () => { const projFs = loadProjectFromDisk("tests/projects/transitiveReferences"); const allExpectedOutputs = [ "/src/a.js", "/src/a.d.ts", @@ -460,7 +460,7 @@ export const b = new A();`); export namespace OutFile { const outFileFs = loadProjectFromDisk("tests/projects/outfile-concat"); - describe("tsbuild - baseline sectioned sourcemaps", () => { + describe("unittests:: tsbuild - baseline sectioned sourcemaps", () => { let fs: vfs.FileSystem | undefined; before(() => { fs = outFileFs.shadow(); @@ -480,7 +480,7 @@ export const b = new A();`); }); }); - describe("tsbuild - downstream prepend projects always get rebuilt", () => { + describe("unittests:: tsbuild - downstream prepend projects always get rebuilt", () => { it("", () => { const fs = outFileFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -508,7 +508,7 @@ export const b = new A();`); "/src/core/index.d.ts.map", ]; - describe("tsbuild - empty files option in tsconfig", () => { + describe("unittests:: tsbuild - empty files option in tsconfig", () => { it("has empty files diagnostic when files is empty and no references are provided", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -541,7 +541,7 @@ export const b = new A();`); }); } - describe("tsbuild - graph-ordering", () => { + describe("unittests:: tsbuild - graph-ordering", () => { let host: fakes.SolutionBuilderHost | undefined; const deps: [string, string][] = [ ["A", "B"], diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index 9f45da51656..72da8dbf85e 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -14,7 +14,7 @@ namespace ts.tscWatch { return solutionBuilder; } - describe("tsbuild-watch program updates", () => { + describe("unittests:: tsbuild-watch program updates", () => { const project = "sample1"; const enum SubProject { core = "core", diff --git a/src/testRunner/unittests/tscWatch/emit.ts b/src/testRunner/unittests/tscWatch/emit.ts index 2f7a70bd2c6..06aec75f31f 100644 --- a/src/testRunner/unittests/tscWatch/emit.ts +++ b/src/testRunner/unittests/tscWatch/emit.ts @@ -38,7 +38,7 @@ namespace ts.tscWatch { checkOutputDoesNotContain(host, expectedNonAffectedFiles); } - describe("tsc-watch:: emit with outFile or out setting", () => { + describe("unittests:: tsc-watch:: emit with outFile or out setting", () => { function createWatchForOut(out?: string, outFile?: string) { const host = createWatchedSystem([]); const config: FileOrFolderEmit = { @@ -161,7 +161,7 @@ namespace ts.tscWatch { }); }); - describe("tsc-watch:: emit for configured projects", () => { + describe("unittests:: tsc-watch:: emit for configured projects", () => { const file1Consumer1Path = "/a/b/file1Consumer1.ts"; const moduleFile1Path = "/a/b/moduleFile1.ts"; const configFilePath = "/a/b/tsconfig.json"; @@ -495,7 +495,7 @@ namespace ts.tscWatch { }); }); - describe("tsc-watch:: emit file content", () => { + describe("unittests:: tsc-watch:: emit file content", () => { interface EmittedFile extends File { shouldBeWritten: boolean; } @@ -676,7 +676,7 @@ namespace ts.tscWatch { }); }); - describe("tsc-watch:: emit with when module emit is specified as node", () => { + describe("unittests:: tsc-watch:: emit with when module emit is specified as node", () => { it("when instead of filechanged recursive directory watcher is invoked", () => { const configFile: File = { path: "/a/rootFolder/project/tsconfig.json", diff --git a/src/testRunner/unittests/tscWatch/resolutionCache.ts b/src/testRunner/unittests/tscWatch/resolutionCache.ts index e7b546edcd9..b29fde85697 100644 --- a/src/testRunner/unittests/tscWatch/resolutionCache.ts +++ b/src/testRunner/unittests/tscWatch/resolutionCache.ts @@ -1,5 +1,5 @@ namespace ts.tscWatch { - describe("tsc-watch:: resolutionCache:: tsc-watch module resolution caching", () => { + describe("unittests:: tsc-watch:: resolutionCache:: tsc-watch module resolution caching", () => { it("works", () => { const root = { path: "/a/d/f0.ts", @@ -404,7 +404,7 @@ declare module "fs" { }); }); - describe("tsc-watch:: resolutionCache:: tsc-watch with modules linked to sibling folder", () => { + describe("unittests:: tsc-watch:: resolutionCache:: tsc-watch with modules linked to sibling folder", () => { const projectRoot = "/user/username/projects/project"; const mainPackageRoot = `${projectRoot}/main`; const linkedPackageRoot = `${projectRoot}/linked-package`; diff --git a/src/testRunner/unittests/tscWatch/watchApi.ts b/src/testRunner/unittests/tscWatch/watchApi.ts index 334b353f1d5..35527b73701 100644 --- a/src/testRunner/unittests/tscWatch/watchApi.ts +++ b/src/testRunner/unittests/tscWatch/watchApi.ts @@ -1,5 +1,5 @@ namespace ts.tscWatch { - describe("tsc-watch:: watchAPI:: tsc-watch with custom module resolution", () => { + describe("unittests:: tsc-watch:: watchAPI:: tsc-watch with custom module resolution", () => { const projectRoot = "/user/username/projects/project"; const configFileJson: any = { compilerOptions: { module: "commonjs", resolveJsonModule: true }, diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts index 3d09e2a38f6..d30e5854652 100644 --- a/src/testRunner/unittests/tscWatch/watchEnvironment.ts +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -1,6 +1,6 @@ namespace ts.tscWatch { import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; - describe("tsc-watch:: watchEnvironment:: tsc-watch with different polling/non polling options", () => { + describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different polling/non polling options", () => { it("watchFile using dynamic priority polling", () => { const projectFolder = "/a/username/project"; const file1: File = { diff --git a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts index b5132f57c0e..26b7857347a 100644 --- a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts +++ b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts @@ -3,7 +3,7 @@ namespace ts.projectSystem { return countWhere(recursiveWatchedDirs, dir => file.length > dir.length && startsWith(file, dir) && file[dir.length] === directorySeparator); } - describe("tsserver:: CachingFileSystemInformation:: tsserverProjectSystem CachingFileSystemInformation", () => { + describe("unittests:: tsserver:: CachingFileSystemInformation:: tsserverProjectSystem CachingFileSystemInformation", () => { enum CalledMapsWithSingleArg { fileExists = "fileExists", directoryExists = "directoryExists", diff --git a/src/testRunner/unittests/tsserver/cancellationToken.ts b/src/testRunner/unittests/tsserver/cancellationToken.ts index 5d9cd5527e6..71fb4bb4b22 100644 --- a/src/testRunner/unittests/tsserver/cancellationToken.ts +++ b/src/testRunner/unittests/tsserver/cancellationToken.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: cancellationToken", () => { + describe("unittests:: tsserver:: cancellationToken", () => { // Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test let oldPrepare: AnyFunction; before(() => { diff --git a/src/testRunner/unittests/tsserver/compileOnSave.ts b/src/testRunner/unittests/tsserver/compileOnSave.ts index 310a6e9e33c..667144f6a06 100644 --- a/src/testRunner/unittests/tsserver/compileOnSave.ts +++ b/src/testRunner/unittests/tsserver/compileOnSave.ts @@ -6,7 +6,7 @@ namespace ts.projectSystem { return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host); } - describe("tsserver:: compileOnSave:: affected list", () => { + describe("unittests:: tsserver:: compileOnSave:: affected list", () => { function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: File[] }[]) { const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[]; const actualResult = response.sort((list1, list2) => compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); @@ -552,7 +552,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: compileOnSave:: EmitFile test", () => { + describe("unittests:: tsserver:: compileOnSave:: EmitFile test", () => { it("should respect line endings", () => { test("\n"); test("\r\n"); diff --git a/src/testRunner/unittests/tsserver/completions.ts b/src/testRunner/unittests/tsserver/completions.ts index ed020bae0ba..0a62f74dc7d 100644 --- a/src/testRunner/unittests/tsserver/completions.ts +++ b/src/testRunner/unittests/tsserver/completions.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: completions", () => { + describe("unittests:: tsserver:: completions", () => { it("works", () => { const aTs: File = { path: "/a.ts", diff --git a/src/testRunner/unittests/tsserver/configFileSearch.ts b/src/testRunner/unittests/tsserver/configFileSearch.ts index 4d0b0c0efdd..c9e02b8bd06 100644 --- a/src/testRunner/unittests/tsserver/configFileSearch.ts +++ b/src/testRunner/unittests/tsserver/configFileSearch.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: searching for config file", () => { + describe("unittests:: tsserver:: searching for config file", () => { it("should stop at projectRootPath if given", () => { const f1 = { path: "/a/file1.ts", diff --git a/src/testRunner/unittests/tsserver/declarationFileMaps.ts b/src/testRunner/unittests/tsserver/declarationFileMaps.ts index 334af82ca48..200d7af0240 100644 --- a/src/testRunner/unittests/tsserver/declarationFileMaps.ts +++ b/src/testRunner/unittests/tsserver/declarationFileMaps.ts @@ -40,7 +40,7 @@ namespace ts.projectSystem { assert.deepEqual(output.outputFiles, expectedFiles.map((e): OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false }))); } - describe("tsserver:: with declaration file maps:: project references", () => { + describe("unittests:: tsserver:: with declaration file maps:: project references", () => { const aTs: File = { path: "/a/a.ts", content: "export function fnA() {}\nexport interface IfaceA {}\nexport const instanceA: IfaceA = {};", diff --git a/src/testRunner/unittests/tsserver/documentRegistry.ts b/src/testRunner/unittests/tsserver/documentRegistry.ts index 0fd812a89d2..1761e413833 100644 --- a/src/testRunner/unittests/tsserver/documentRegistry.ts +++ b/src/testRunner/unittests/tsserver/documentRegistry.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: document registry in project service", () => { + describe("unittests:: tsserver:: document registry in project service", () => { const projectRootPath = "/user/username/projects/project"; const importModuleContent = `import {a} from "./module1"`; const file: File = { diff --git a/src/testRunner/unittests/tsserver/duplicatePackages.ts b/src/testRunner/unittests/tsserver/duplicatePackages.ts index f8a282ee7d2..ec85eb6a82f 100644 --- a/src/testRunner/unittests/tsserver/duplicatePackages.ts +++ b/src/testRunner/unittests/tsserver/duplicatePackages.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: duplicate packages", () => { + describe("unittests:: tsserver:: duplicate packages", () => { // Tests that 'moduleSpecifiers.ts' will import from the redirecting file, and not from the file it redirects to, if that can provide a global module specifier. it("works with import fixes", () => { const packageContent = "export const foo: number;"; diff --git a/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts b/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts index ca7f839e1b0..ff69a7e6568 100644 --- a/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts +++ b/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: LargeFileReferencedEvent with large file", () => { + describe("unittests:: tsserver:: events:: LargeFileReferencedEvent with large file", () => { const projectRoot = "/user/username/projects/project"; function getLargeFile(useLargeTsFile: boolean) { diff --git a/src/testRunner/unittests/tsserver/events/projectLoading.ts b/src/testRunner/unittests/tsserver/events/projectLoading.ts index 2de3ab40db6..7a881ff1380 100644 --- a/src/testRunner/unittests/tsserver/events/projectLoading.ts +++ b/src/testRunner/unittests/tsserver/events/projectLoading.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: events:: ProjectLoadingStart and ProjectLoadingFinish events", () => { + describe("unittests:: tsserver:: events:: ProjectLoadingStart and ProjectLoadingFinish events", () => { const projectRoot = "/user/username/projects"; const aTs: File = { path: `${projectRoot}/a/a.ts`, diff --git a/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts b/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts index c818f4d2a8d..05a43cb4a15 100644 --- a/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts +++ b/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: ProjectsUpdatedInBackground", () => { + describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => { function verifyFiles(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`); const seen = createMap(); diff --git a/src/testRunner/unittests/tsserver/externalProjects.ts b/src/testRunner/unittests/tsserver/externalProjects.ts index 6c246a2e3bd..87615b24d12 100644 --- a/src/testRunner/unittests/tsserver/externalProjects.ts +++ b/src/testRunner/unittests/tsserver/externalProjects.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: ExternalProjects", () => { + describe("unittests:: tsserver:: ExternalProjects", () => { describe("correctly handling add/remove tsconfig - 1", () => { function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { const f1 = { diff --git a/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts b/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts index d790a1aa25a..bb1be544a2c 100644 --- a/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts +++ b/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: forceConsistentCasingInFileNames", () => { + describe("unittests:: tsserver:: forceConsistentCasingInFileNames", () => { it("works when extends is specified with a case insensitive file system", () => { const rootPath = "/Users/username/dev/project"; const file1: File = { diff --git a/src/testRunner/unittests/tsserver/formatSettings.ts b/src/testRunner/unittests/tsserver/formatSettings.ts index 4e3e2ebd418..2d09ed8a8a0 100644 --- a/src/testRunner/unittests/tsserver/formatSettings.ts +++ b/src/testRunner/unittests/tsserver/formatSettings.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: format settings", () => { + describe("unittests:: tsserver:: format settings", () => { it("can be set globally", () => { const f1 = { path: "/a/b/app.ts", diff --git a/src/testRunner/unittests/tsserver/getEditsForFileRename.ts b/src/testRunner/unittests/tsserver/getEditsForFileRename.ts index 3fadf99c3f8..2b28de7b121 100644 --- a/src/testRunner/unittests/tsserver/getEditsForFileRename.ts +++ b/src/testRunner/unittests/tsserver/getEditsForFileRename.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: getEditsForFileRename", () => { + describe("unittests:: tsserver:: getEditsForFileRename", () => { it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => { const userTs: File = { path: "/user.ts", diff --git a/src/testRunner/unittests/tsserver/importHelpers.ts b/src/testRunner/unittests/tsserver/importHelpers.ts index 42e38aed122..b1ec5955eaa 100644 --- a/src/testRunner/unittests/tsserver/importHelpers.ts +++ b/src/testRunner/unittests/tsserver/importHelpers.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: import helpers", () => { + describe("unittests:: tsserver:: import helpers", () => { it("should not crash in tsserver", () => { const f1 = { path: "/a/app.ts", diff --git a/src/testRunner/unittests/tsserver/inferredProjects.ts b/src/testRunner/unittests/tsserver/inferredProjects.ts index b22413c6ab3..47e871efb82 100644 --- a/src/testRunner/unittests/tsserver/inferredProjects.ts +++ b/src/testRunner/unittests/tsserver/inferredProjects.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: Inferred projects", () => { + describe("unittests:: tsserver:: Inferred projects", () => { it("should support files without extensions", () => { const f = { path: "/a/compile", diff --git a/src/testRunner/unittests/tsserver/languageService.ts b/src/testRunner/unittests/tsserver/languageService.ts index 6ee68e74c15..86d426664df 100644 --- a/src/testRunner/unittests/tsserver/languageService.ts +++ b/src/testRunner/unittests/tsserver/languageService.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: Language service", () => { + describe("unittests:: tsserver:: Language service", () => { it("should work correctly on case-sensitive file systems", () => { const lib = { path: "/a/Lib/lib.d.ts", diff --git a/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts b/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts index dd0b954824d..eb254789d67 100644 --- a/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts +++ b/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: maxNodeModuleJsDepth for inferred projects", () => { + describe("unittests:: tsserver:: maxNodeModuleJsDepth for inferred projects", () => { it("should be set to 2 if the project has js root files", () => { const file1: File = { path: "/a/b/file1.js", diff --git a/src/testRunner/unittests/tsserver/metadataInResponse.ts b/src/testRunner/unittests/tsserver/metadataInResponse.ts index 783fce4dd16..6ec8a565d87 100644 --- a/src/testRunner/unittests/tsserver/metadataInResponse.ts +++ b/src/testRunner/unittests/tsserver/metadataInResponse.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: with metadata in response", () => { + describe("unittests:: tsserver:: with metadata in response", () => { const metadata = "Extra Info"; function verifyOutput(host: TestServerHost, expectedResponse: protocol.Response) { const output = host.getOutput().map(mapOutputToJson); diff --git a/src/testRunner/unittests/tsserver/navTo.ts b/src/testRunner/unittests/tsserver/navTo.ts index 5d45e537ede..b3aebef1043 100644 --- a/src/testRunner/unittests/tsserver/navTo.ts +++ b/src/testRunner/unittests/tsserver/navTo.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: navigate-to for javascript project", () => { + describe("unittests:: tsserver:: navigate-to for javascript project", () => { function containsNavToItem(items: protocol.NavtoItem[], itemName: string, itemKind: string) { return find(items, item => item.name === itemName && item.kind === itemKind) !== undefined; } diff --git a/src/testRunner/unittests/tsserver/occurences.ts b/src/testRunner/unittests/tsserver/occurences.ts index bcdadd3872c..cf9c83e2e72 100644 --- a/src/testRunner/unittests/tsserver/occurences.ts +++ b/src/testRunner/unittests/tsserver/occurences.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: occurence highlight on string", () => { + describe("unittests:: tsserver:: occurence highlight on string", () => { it("should be marked if only on string values", () => { const file1: File = { path: "/a/b/file1.ts", diff --git a/src/testRunner/unittests/tsserver/openFile.ts b/src/testRunner/unittests/tsserver/openFile.ts index 187b5043fcc..d2a995c68f8 100644 --- a/src/testRunner/unittests/tsserver/openFile.ts +++ b/src/testRunner/unittests/tsserver/openFile.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: Open-file", () => { + describe("unittests:: tsserver:: Open-file", () => { it("can be reloaded with empty content", () => { const f = { path: "/a/b/app.ts", diff --git a/src/testRunner/unittests/tsserver/projectErrors.ts b/src/testRunner/unittests/tsserver/projectErrors.ts index cb82a9eeca6..9e24771f3ae 100644 --- a/src/testRunner/unittests/tsserver/projectErrors.ts +++ b/src/testRunner/unittests/tsserver/projectErrors.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: Project Errors", () => { + describe("unittests:: tsserver:: Project Errors", () => { function checkProjectErrors(projectFiles: server.ProjectFilesWithTSDiagnostics, expectedErrors: ReadonlyArray): void { assert.isTrue(projectFiles !== undefined, "missing project files"); checkProjectErrorsWorker(projectFiles.projectErrors, expectedErrors); @@ -200,7 +200,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: Project Errors are reported as appropriate", () => { + describe("unittests:: tsserver:: Project Errors are reported as appropriate", () => { function createErrorLogger() { let hasError = false; const errorLogger: server.Logger = { @@ -481,7 +481,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: Project Errors for Configure file diagnostics events", () => { + describe("unittests:: tsserver:: Project Errors for Configure file diagnostics events", () => { function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string): ConfigFileDiagnostic { const d = Diagnostics.Unknown_compiler_option_0; const start = configFile.content.indexOf(prop) - 1; // start at "prop" @@ -671,7 +671,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: Project Errors dont include overwrite emit error", () => { + describe("unittests:: tsserver:: Project Errors dont include overwrite emit error", () => { it("for inferred project", () => { const f1 = { path: "/a/b/f1.js", @@ -754,7 +754,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: Project Errors reports Options Diagnostic locations correctly with changes in configFile contents", () => { + describe("unittests:: tsserver:: Project Errors reports Options Diagnostic locations correctly with changes in configFile contents", () => { it("when options change", () => { const file = { path: "/a/b/app.ts", @@ -818,7 +818,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: Project Errors with config file change", () => { + describe("unittests:: tsserver:: Project Errors with config file change", () => { it("Updates diagnostics when '--noUnusedLabels' changes", () => { const aTs: File = { path: "/a.ts", content: "label: while (1) {}" }; const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`; diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts index c7a42d6b813..e39c510216d 100644 --- a/src/testRunner/unittests/tsserver/projectReferences.ts +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: with project references and tsbuild", () => { + describe("unittests:: tsserver:: with project references and tsbuild", () => { function createHost(files: ReadonlyArray, rootNames: ReadonlyArray) { const host = createServerHost(files); diff --git a/src/testRunner/unittests/tsserver/refactors.ts b/src/testRunner/unittests/tsserver/refactors.ts index 2e916f50f9b..2f4531f61a5 100644 --- a/src/testRunner/unittests/tsserver/refactors.ts +++ b/src/testRunner/unittests/tsserver/refactors.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: refactors", () => { + describe("unittests:: tsserver:: refactors", () => { it("use formatting options", () => { const file = { path: "/a.ts", diff --git a/src/testRunner/unittests/tsserver/reload.ts b/src/testRunner/unittests/tsserver/reload.ts index 80f3e56e437..e9a32a5e1a1 100644 --- a/src/testRunner/unittests/tsserver/reload.ts +++ b/src/testRunner/unittests/tsserver/reload.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: reload", () => { + describe("unittests:: tsserver:: reload", () => { it("should work with temp file", () => { const f1 = { path: "/a/b/app.ts", diff --git a/src/testRunner/unittests/tsserver/rename.ts b/src/testRunner/unittests/tsserver/rename.ts index 3b4d77263e6..75c08bb8bb0 100644 --- a/src/testRunner/unittests/tsserver/rename.ts +++ b/src/testRunner/unittests/tsserver/rename.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: rename", () => { + describe("unittests:: tsserver:: rename", () => { it("works with fileToRename", () => { const aTs: File = { path: "/a.ts", content: "export const a = 0;" }; const bTs: File = { path: "/b.ts", content: 'import { a } from "./a";' }; diff --git a/src/testRunner/unittests/tsserver/resolutionCache.ts b/src/testRunner/unittests/tsserver/resolutionCache.ts index 9f75455e226..33b59e2faae 100644 --- a/src/testRunner/unittests/tsserver/resolutionCache.ts +++ b/src/testRunner/unittests/tsserver/resolutionCache.ts @@ -5,7 +5,7 @@ namespace ts.projectSystem { return resolutionTrace; } - describe("tsserver:: resolutionCache:: tsserverProjectSystem extra resolution pass in lshost", () => { + describe("unittests:: tsserver:: resolutionCache:: tsserverProjectSystem extra resolution pass in lshost", () => { it("can load typings that are proper modules", () => { const file1 = { path: "/a/b/app.js", @@ -46,7 +46,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: resolutionCache:: tsserverProjectSystem watching @types", () => { + describe("unittests:: tsserver:: resolutionCache:: tsserverProjectSystem watching @types", () => { it("works correctly when typings are added or removed", () => { const f1 = { path: "/a/b/app.ts", @@ -92,7 +92,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: resolutionCache:: tsserverProjectSystem add the missing module file for inferred project", () => { + describe("unittests:: tsserver:: resolutionCache:: tsserverProjectSystem add the missing module file for inferred project", () => { it("should remove the `module not found` error", () => { const moduleFile = { path: "/a/b/moduleFile.ts", @@ -358,7 +358,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: resolutionCache:: tsserverProjectSystem rename a module file and rename back", () => { + describe("unittests:: tsserver:: resolutionCache:: tsserverProjectSystem rename a module file and rename back", () => { it("should restore the states for inferred projects", () => { const moduleFile = { path: "/a/b/moduleFile.ts", @@ -493,7 +493,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: resolutionCache:: tsserverProjectSystem module resolution caching", () => { + describe("unittests:: tsserver:: resolutionCache:: tsserverProjectSystem module resolution caching", () => { const projectLocation = "/user/username/projects/myproject"; const configFile: File = { path: `${projectLocation}/tsconfig.json`, diff --git a/src/testRunner/unittests/tsserver/session.ts b/src/testRunner/unittests/tsserver/session.ts index 927b22fb475..bf21228405c 100644 --- a/src/testRunner/unittests/tsserver/session.ts +++ b/src/testRunner/unittests/tsserver/session.ts @@ -35,7 +35,7 @@ namespace ts.server { } } - describe("tsserver:: Session:: General functionality", () => { + describe("unittests:: tsserver:: Session:: General functionality", () => { let session: TestSession; let lastSent: protocol.Message; @@ -418,7 +418,7 @@ namespace ts.server { }); }); - describe("tsserver:: Session:: exceptions", () => { + describe("unittests:: tsserver:: Session:: exceptions", () => { // Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test let oldPrepare: AnyFunction; @@ -489,7 +489,7 @@ namespace ts.server { }); }); - describe("tsserver:: Session:: how Session is extendable via subclassing", () => { + describe("unittests:: tsserver:: Session:: how Session is extendable via subclassing", () => { class TestSession extends Session { lastSent: protocol.Message | undefined; customHandler = "testhandler"; @@ -558,7 +558,7 @@ namespace ts.server { }); }); - describe("tsserver:: Session:: an example of using the Session API to create an in-process server", () => { + describe("unittests:: tsserver:: Session:: an example of using the Session API to create an in-process server", () => { class InProcSession extends Session { private queue: protocol.Request[] = []; constructor(private client: InProcClient) { @@ -710,7 +710,7 @@ namespace ts.server { }); }); - describe("tsserver:: Session:: helpers", () => { + describe("unittests:: tsserver:: Session:: helpers", () => { it(getLocationInNewDocument.name, () => { const text = `// blank line\nconst x = 0;`; const renameLocationInOldText = text.indexOf("0"); diff --git a/src/testRunner/unittests/tsserver/skipLibCheck.ts b/src/testRunner/unittests/tsserver/skipLibCheck.ts index 463ea439396..d154154f083 100644 --- a/src/testRunner/unittests/tsserver/skipLibCheck.ts +++ b/src/testRunner/unittests/tsserver/skipLibCheck.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: with skipLibCheck", () => { + describe("unittests:: tsserver:: with skipLibCheck", () => { it("should be turned on for js-only inferred projects", () => { const file1 = { path: "/a/b/file1.js", diff --git a/src/testRunner/unittests/tsserver/symLinks.ts b/src/testRunner/unittests/tsserver/symLinks.ts index d6fa9dca894..e331c94ca1a 100644 --- a/src/testRunner/unittests/tsserver/symLinks.ts +++ b/src/testRunner/unittests/tsserver/symLinks.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: symLinks", () => { + describe("unittests:: tsserver:: symLinks", () => { it("rename in common file renames all project", () => { const projects = "/users/username/projects"; const folderA = `${projects}/a`; diff --git a/src/testRunner/unittests/tsserver/syntaxOperations.ts b/src/testRunner/unittests/tsserver/syntaxOperations.ts index c2ec877137a..66a962d3625 100644 --- a/src/testRunner/unittests/tsserver/syntaxOperations.ts +++ b/src/testRunner/unittests/tsserver/syntaxOperations.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: syntax operations", () => { + describe("unittests:: tsserver:: syntax operations", () => { function navBarFull(session: TestSession, file: File) { return JSON.stringify(session.executeCommandSeq({ command: protocol.CommandTypes.NavBarFull, diff --git a/src/testRunner/unittests/tsserver/telemetry.ts b/src/testRunner/unittests/tsserver/telemetry.ts index 4f4d8ba8b19..cdf78adbf54 100644 --- a/src/testRunner/unittests/tsserver/telemetry.ts +++ b/src/testRunner/unittests/tsserver/telemetry.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: project telemetry", () => { + describe("unittests:: tsserver:: project telemetry", () => { it("does nothing for inferred project", () => { const file = makeFile("/a.js"); const et = new TestServerEventManager([file]); diff --git a/src/testRunner/unittests/tsserver/textStorage.ts b/src/testRunner/unittests/tsserver/textStorage.ts index 157734da931..40424b5c9c3 100644 --- a/src/testRunner/unittests/tsserver/textStorage.ts +++ b/src/testRunner/unittests/tsserver/textStorage.ts @@ -1,5 +1,5 @@ namespace ts.textStorage { - describe("tsserver:: Text storage", () => { + describe("unittests:: tsserver:: Text storage", () => { const f = { path: "/a/app.ts", content: ` diff --git a/src/testRunner/unittests/tsserver/typeAquisition.ts b/src/testRunner/unittests/tsserver/typeAquisition.ts index 9f487896e03..ac9d3e3cfc0 100644 --- a/src/testRunner/unittests/tsserver/typeAquisition.ts +++ b/src/testRunner/unittests/tsserver/typeAquisition.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: autoDiscovery", () => { + describe("unittests:: tsserver:: autoDiscovery", () => { it("does not depend on extension", () => { const file1 = { path: "/a/b/app.html", @@ -22,7 +22,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: prefer typings to js", () => { + describe("unittests:: tsserver:: prefer typings to js", () => { it("during second resolution pass", () => { const typingsCacheLocation = "/a/typings"; const f1 = { diff --git a/src/testRunner/unittests/tsserver/typeReferenceDirectives.ts b/src/testRunner/unittests/tsserver/typeReferenceDirectives.ts index 8318f402a1a..e3ad121e427 100644 --- a/src/testRunner/unittests/tsserver/typeReferenceDirectives.ts +++ b/src/testRunner/unittests/tsserver/typeReferenceDirectives.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: typeReferenceDirectives", () => { + describe("unittests:: tsserver:: typeReferenceDirectives", () => { it("when typeReferenceDirective contains UpperCasePackage", () => { const projectLocation = "/user/username/projects/myproject"; const libProjectLocation = `${projectLocation}/lib`; diff --git a/src/testRunner/unittests/tsserver/typingsInstaller.ts b/src/testRunner/unittests/tsserver/typingsInstaller.ts index 18117a5f961..76df9934682 100644 --- a/src/testRunner/unittests/tsserver/typingsInstaller.ts +++ b/src/testRunner/unittests/tsserver/typingsInstaller.ts @@ -47,7 +47,7 @@ namespace ts.projectSystem { import typingsName = TI.typingsName; - describe("tsserver:: typingsInstaller:: local module", () => { + describe("unittests:: tsserver:: typingsInstaller:: local module", () => { it("should not be picked up", () => { const f1 = { path: "/a/app.js", @@ -86,7 +86,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: typingsInstaller:: General functionality", () => { + describe("unittests:: tsserver:: typingsInstaller:: General functionality", () => { it("configured projects (typings installed) 1", () => { const file1 = { path: "/a/b/app.js", @@ -1249,7 +1249,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: typingsInstaller:: Validate package name:", () => { + describe("unittests:: tsserver:: typingsInstaller:: Validate package name:", () => { it("name cannot be too long", () => { let packageName = "a"; for (let i = 0; i < 8; i++) { @@ -1273,7 +1273,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: typingsInstaller:: Invalid package names", () => { + describe("unittests:: tsserver:: typingsInstaller:: Invalid package names", () => { it("should not be installed", () => { const f1 = { path: "/a/b/app.js", @@ -1305,7 +1305,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: typingsInstaller:: discover typings", () => { + describe("unittests:: tsserver:: typingsInstaller:: discover typings", () => { const emptySafeList = emptyMap; it("should use mappings from safe list", () => { @@ -1517,7 +1517,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: typingsInstaller:: telemetry events", () => { + describe("unittests:: tsserver:: typingsInstaller:: telemetry events", () => { it("should be received", () => { const f1 = { path: "/a/app.js", @@ -1567,7 +1567,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: typingsInstaller:: progress notifications", () => { + describe("unittests:: tsserver:: typingsInstaller:: progress notifications", () => { it("should be sent for success", () => { const f1 = { path: "/a/app.js", @@ -1676,7 +1676,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: typingsInstaller:: npm installation command", () => { + describe("unittests:: tsserver:: typingsInstaller:: npm installation command", () => { const npmPath = "npm", tsVersion = "2.9.0-dev.20180410"; const packageNames = ["@types/graphql@ts2.8", "@types/highlight.js@ts2.8", "@types/jest@ts2.8", "@types/mini-css-extract-plugin@ts2.8", "@types/mongoose@ts2.8", "@types/pg@ts2.8", "@types/webpack-bundle-analyzer@ts2.8", "@types/enhanced-resolve@ts2.8", "@types/eslint-plugin-prettier@ts2.8", "@types/friendly-errors-webpack-plugin@ts2.8", "@types/hammerjs@ts2.8", "@types/history@ts2.8", "@types/image-size@ts2.8", "@types/js-cookie@ts2.8", "@types/koa-compress@ts2.8", "@types/less@ts2.8", "@types/material-ui@ts2.8", "@types/mysql@ts2.8", "@types/nodemailer@ts2.8", "@types/prettier@ts2.8", "@types/query-string@ts2.8", "@types/react-places-autocomplete@ts2.8", "@types/react-router@ts2.8", "@types/react-router-config@ts2.8", "@types/react-select@ts2.8", "@types/react-transition-group@ts2.8", "@types/redux-form@ts2.8", "@types/abbrev@ts2.8", "@types/accepts@ts2.8", "@types/acorn@ts2.8", "@types/ansi-regex@ts2.8", "@types/ansi-styles@ts2.8", "@types/anymatch@ts2.8", "@types/apollo-codegen@ts2.8", "@types/are-we-there-yet@ts2.8", "@types/argparse@ts2.8", "@types/arr-union@ts2.8", "@types/array-find-index@ts2.8", "@types/array-uniq@ts2.8", "@types/array-unique@ts2.8", "@types/arrify@ts2.8", "@types/assert-plus@ts2.8", "@types/async@ts2.8", "@types/autoprefixer@ts2.8", "@types/aws4@ts2.8", "@types/babel-code-frame@ts2.8", "@types/babel-generator@ts2.8", "@types/babel-plugin-syntax-jsx@ts2.8", "@types/babel-template@ts2.8", "@types/babel-traverse@ts2.8", "@types/babel-types@ts2.8", "@types/babylon@ts2.8", "@types/base64-js@ts2.8", "@types/basic-auth@ts2.8", "@types/big.js@ts2.8", "@types/bl@ts2.8", "@types/bluebird@ts2.8", "@types/body-parser@ts2.8", "@types/bonjour@ts2.8", "@types/boom@ts2.8", "@types/brace-expansion@ts2.8", "@types/braces@ts2.8", "@types/brorand@ts2.8", "@types/browser-resolve@ts2.8", "@types/bson@ts2.8", "@types/buffer-equal@ts2.8", "@types/builtin-modules@ts2.8", "@types/bytes@ts2.8", "@types/callsites@ts2.8", "@types/camelcase@ts2.8", "@types/camelcase-keys@ts2.8", "@types/caseless@ts2.8", "@types/change-emitter@ts2.8", "@types/check-types@ts2.8", "@types/cheerio@ts2.8", "@types/chokidar@ts2.8", "@types/chownr@ts2.8", "@types/circular-json@ts2.8", "@types/classnames@ts2.8", "@types/clean-css@ts2.8", "@types/clone@ts2.8", "@types/co-body@ts2.8", "@types/color@ts2.8", "@types/color-convert@ts2.8", "@types/color-name@ts2.8", "@types/color-string@ts2.8", "@types/colors@ts2.8", "@types/combined-stream@ts2.8", "@types/common-tags@ts2.8", "@types/component-emitter@ts2.8", "@types/compressible@ts2.8", "@types/compression@ts2.8", "@types/concat-stream@ts2.8", "@types/connect-history-api-fallback@ts2.8", "@types/content-disposition@ts2.8", "@types/content-type@ts2.8", "@types/convert-source-map@ts2.8", "@types/cookie@ts2.8", "@types/cookie-signature@ts2.8", "@types/cookies@ts2.8", "@types/core-js@ts2.8", "@types/cosmiconfig@ts2.8", "@types/create-react-class@ts2.8", "@types/cross-spawn@ts2.8", "@types/cryptiles@ts2.8", "@types/css-modules-require-hook@ts2.8", "@types/dargs@ts2.8", "@types/dateformat@ts2.8", "@types/debug@ts2.8", "@types/decamelize@ts2.8", "@types/decompress@ts2.8", "@types/decompress-response@ts2.8", "@types/deep-equal@ts2.8", "@types/deep-extend@ts2.8", "@types/deepmerge@ts2.8", "@types/defined@ts2.8", "@types/del@ts2.8", "@types/depd@ts2.8", "@types/destroy@ts2.8", "@types/detect-indent@ts2.8", "@types/detect-newline@ts2.8", "@types/diff@ts2.8", "@types/doctrine@ts2.8", "@types/download@ts2.8", "@types/draft-js@ts2.8", "@types/duplexer2@ts2.8", "@types/duplexer3@ts2.8", "@types/duplexify@ts2.8", "@types/ejs@ts2.8", "@types/end-of-stream@ts2.8", "@types/entities@ts2.8", "@types/escape-html@ts2.8", "@types/escape-string-regexp@ts2.8", "@types/escodegen@ts2.8", "@types/eslint-scope@ts2.8", "@types/eslint-visitor-keys@ts2.8", "@types/esprima@ts2.8", "@types/estraverse@ts2.8", "@types/etag@ts2.8", "@types/events@ts2.8", "@types/execa@ts2.8", "@types/exenv@ts2.8", "@types/exit@ts2.8", "@types/exit-hook@ts2.8", "@types/expect@ts2.8", "@types/express@ts2.8", "@types/express-graphql@ts2.8", "@types/extend@ts2.8", "@types/extract-zip@ts2.8", "@types/fancy-log@ts2.8", "@types/fast-diff@ts2.8", "@types/fast-levenshtein@ts2.8", "@types/figures@ts2.8", "@types/file-type@ts2.8", "@types/filenamify@ts2.8", "@types/filesize@ts2.8", "@types/finalhandler@ts2.8", "@types/find-root@ts2.8", "@types/find-up@ts2.8", "@types/findup-sync@ts2.8", "@types/forever-agent@ts2.8", "@types/form-data@ts2.8", "@types/forwarded@ts2.8", "@types/fresh@ts2.8", "@types/from2@ts2.8", "@types/fs-extra@ts2.8", "@types/get-caller-file@ts2.8", "@types/get-stdin@ts2.8", "@types/get-stream@ts2.8", "@types/get-value@ts2.8", "@types/glob-base@ts2.8", "@types/glob-parent@ts2.8", "@types/glob-stream@ts2.8", "@types/globby@ts2.8", "@types/globule@ts2.8", "@types/got@ts2.8", "@types/graceful-fs@ts2.8", "@types/gulp-rename@ts2.8", "@types/gulp-sourcemaps@ts2.8", "@types/gulp-util@ts2.8", "@types/gzip-size@ts2.8", "@types/handlebars@ts2.8", "@types/has-ansi@ts2.8", "@types/hasha@ts2.8", "@types/he@ts2.8", "@types/hoek@ts2.8", "@types/html-entities@ts2.8", "@types/html-minifier@ts2.8", "@types/htmlparser2@ts2.8", "@types/http-assert@ts2.8", "@types/http-errors@ts2.8", "@types/http-proxy@ts2.8", "@types/http-proxy-middleware@ts2.8", "@types/indent-string@ts2.8", "@types/inflected@ts2.8", "@types/inherits@ts2.8", "@types/ini@ts2.8", "@types/inline-style-prefixer@ts2.8", "@types/inquirer@ts2.8", "@types/internal-ip@ts2.8", "@types/into-stream@ts2.8", "@types/invariant@ts2.8", "@types/ip@ts2.8", "@types/ip-regex@ts2.8", "@types/is-absolute-url@ts2.8", "@types/is-binary-path@ts2.8", "@types/is-finite@ts2.8", "@types/is-glob@ts2.8", "@types/is-my-json-valid@ts2.8", "@types/is-number@ts2.8", "@types/is-object@ts2.8", "@types/is-path-cwd@ts2.8", "@types/is-path-in-cwd@ts2.8", "@types/is-promise@ts2.8", "@types/is-scoped@ts2.8", "@types/is-stream@ts2.8", "@types/is-svg@ts2.8", "@types/is-url@ts2.8", "@types/is-windows@ts2.8", "@types/istanbul-lib-coverage@ts2.8", "@types/istanbul-lib-hook@ts2.8", "@types/istanbul-lib-instrument@ts2.8", "@types/istanbul-lib-report@ts2.8", "@types/istanbul-lib-source-maps@ts2.8", "@types/istanbul-reports@ts2.8", "@types/jest-diff@ts2.8", "@types/jest-docblock@ts2.8", "@types/jest-get-type@ts2.8", "@types/jest-matcher-utils@ts2.8", "@types/jest-validate@ts2.8", "@types/jpeg-js@ts2.8", "@types/js-base64@ts2.8", "@types/js-string-escape@ts2.8", "@types/js-yaml@ts2.8", "@types/jsbn@ts2.8", "@types/jsdom@ts2.8", "@types/jsesc@ts2.8", "@types/json-parse-better-errors@ts2.8", "@types/json-schema@ts2.8", "@types/json-stable-stringify@ts2.8", "@types/json-stringify-safe@ts2.8", "@types/json5@ts2.8", "@types/jsonfile@ts2.8", "@types/jsontoxml@ts2.8", "@types/jss@ts2.8", "@types/keygrip@ts2.8", "@types/keymirror@ts2.8", "@types/keyv@ts2.8", "@types/klaw@ts2.8", "@types/koa-send@ts2.8", "@types/leven@ts2.8", "@types/listr@ts2.8", "@types/load-json-file@ts2.8", "@types/loader-runner@ts2.8", "@types/loader-utils@ts2.8", "@types/locate-path@ts2.8", "@types/lodash-es@ts2.8", "@types/lodash.assign@ts2.8", "@types/lodash.camelcase@ts2.8", "@types/lodash.clonedeep@ts2.8", "@types/lodash.debounce@ts2.8", "@types/lodash.escape@ts2.8", "@types/lodash.flowright@ts2.8", "@types/lodash.get@ts2.8", "@types/lodash.isarguments@ts2.8", "@types/lodash.isarray@ts2.8", "@types/lodash.isequal@ts2.8", "@types/lodash.isobject@ts2.8", "@types/lodash.isstring@ts2.8", "@types/lodash.keys@ts2.8", "@types/lodash.memoize@ts2.8", "@types/lodash.merge@ts2.8", "@types/lodash.mergewith@ts2.8", "@types/lodash.pick@ts2.8", "@types/lodash.sortby@ts2.8", "@types/lodash.tail@ts2.8", "@types/lodash.template@ts2.8", "@types/lodash.throttle@ts2.8", "@types/lodash.unescape@ts2.8", "@types/lodash.uniq@ts2.8", "@types/log-symbols@ts2.8", "@types/log-update@ts2.8", "@types/loglevel@ts2.8", "@types/loud-rejection@ts2.8", "@types/lru-cache@ts2.8", "@types/make-dir@ts2.8", "@types/map-obj@ts2.8", "@types/media-typer@ts2.8", "@types/mem@ts2.8", "@types/mem-fs@ts2.8", "@types/memory-fs@ts2.8", "@types/meow@ts2.8", "@types/merge-descriptors@ts2.8", "@types/merge-stream@ts2.8", "@types/methods@ts2.8", "@types/micromatch@ts2.8", "@types/mime@ts2.8", "@types/mime-db@ts2.8", "@types/mime-types@ts2.8", "@types/minimatch@ts2.8", "@types/minimist@ts2.8", "@types/minipass@ts2.8", "@types/mkdirp@ts2.8", "@types/mongodb@ts2.8", "@types/morgan@ts2.8", "@types/move-concurrently@ts2.8", "@types/ms@ts2.8", "@types/msgpack-lite@ts2.8", "@types/multimatch@ts2.8", "@types/mz@ts2.8", "@types/negotiator@ts2.8", "@types/node-dir@ts2.8", "@types/node-fetch@ts2.8", "@types/node-forge@ts2.8", "@types/node-int64@ts2.8", "@types/node-ipc@ts2.8", "@types/node-notifier@ts2.8", "@types/nomnom@ts2.8", "@types/nopt@ts2.8", "@types/normalize-package-data@ts2.8", "@types/normalize-url@ts2.8", "@types/number-is-nan@ts2.8", "@types/object-assign@ts2.8", "@types/on-finished@ts2.8", "@types/on-headers@ts2.8", "@types/once@ts2.8", "@types/onetime@ts2.8", "@types/opener@ts2.8", "@types/opn@ts2.8", "@types/optimist@ts2.8", "@types/ora@ts2.8", "@types/os-homedir@ts2.8", "@types/os-locale@ts2.8", "@types/os-tmpdir@ts2.8", "@types/p-cancelable@ts2.8", "@types/p-each-series@ts2.8", "@types/p-event@ts2.8", "@types/p-lazy@ts2.8", "@types/p-limit@ts2.8", "@types/p-locate@ts2.8", "@types/p-map@ts2.8", "@types/p-map-series@ts2.8", "@types/p-reduce@ts2.8", "@types/p-timeout@ts2.8", "@types/p-try@ts2.8", "@types/pako@ts2.8", "@types/parse-glob@ts2.8", "@types/parse-json@ts2.8", "@types/parseurl@ts2.8", "@types/path-exists@ts2.8", "@types/path-is-absolute@ts2.8", "@types/path-parse@ts2.8", "@types/pg-pool@ts2.8", "@types/pg-types@ts2.8", "@types/pify@ts2.8", "@types/pixelmatch@ts2.8", "@types/pkg-dir@ts2.8", "@types/pluralize@ts2.8", "@types/pngjs@ts2.8", "@types/prelude-ls@ts2.8", "@types/pretty-bytes@ts2.8", "@types/pretty-format@ts2.8", "@types/progress@ts2.8", "@types/promise-retry@ts2.8", "@types/proxy-addr@ts2.8", "@types/pump@ts2.8", "@types/q@ts2.8", "@types/qs@ts2.8", "@types/range-parser@ts2.8", "@types/rc@ts2.8", "@types/rc-select@ts2.8", "@types/rc-slider@ts2.8", "@types/rc-tooltip@ts2.8", "@types/rc-tree@ts2.8", "@types/react-event-listener@ts2.8", "@types/react-side-effect@ts2.8", "@types/react-slick@ts2.8", "@types/read-chunk@ts2.8", "@types/read-pkg@ts2.8", "@types/read-pkg-up@ts2.8", "@types/recompose@ts2.8", "@types/recursive-readdir@ts2.8", "@types/relateurl@ts2.8", "@types/replace-ext@ts2.8", "@types/request@ts2.8", "@types/request-promise-native@ts2.8", "@types/require-directory@ts2.8", "@types/require-from-string@ts2.8", "@types/require-relative@ts2.8", "@types/resolve@ts2.8", "@types/resolve-from@ts2.8", "@types/retry@ts2.8", "@types/rx@ts2.8", "@types/rx-lite@ts2.8", "@types/rx-lite-aggregates@ts2.8", "@types/safe-regex@ts2.8", "@types/sane@ts2.8", "@types/sass-graph@ts2.8", "@types/sax@ts2.8", "@types/scriptjs@ts2.8", "@types/semver@ts2.8", "@types/send@ts2.8", "@types/serialize-javascript@ts2.8", "@types/serve-index@ts2.8", "@types/serve-static@ts2.8", "@types/set-value@ts2.8", "@types/shallowequal@ts2.8", "@types/shelljs@ts2.8", "@types/sockjs@ts2.8", "@types/sockjs-client@ts2.8", "@types/source-list-map@ts2.8", "@types/source-map-support@ts2.8", "@types/spdx-correct@ts2.8", "@types/spdy@ts2.8", "@types/split@ts2.8", "@types/sprintf@ts2.8", "@types/sprintf-js@ts2.8", "@types/sqlstring@ts2.8", "@types/sshpk@ts2.8", "@types/stack-utils@ts2.8", "@types/stat-mode@ts2.8", "@types/statuses@ts2.8", "@types/strict-uri-encode@ts2.8", "@types/string-template@ts2.8", "@types/strip-ansi@ts2.8", "@types/strip-bom@ts2.8", "@types/strip-json-comments@ts2.8", "@types/supports-color@ts2.8", "@types/svg2png@ts2.8", "@types/svgo@ts2.8", "@types/table@ts2.8", "@types/tapable@ts2.8", "@types/tar@ts2.8", "@types/temp@ts2.8", "@types/tempfile@ts2.8", "@types/through@ts2.8", "@types/through2@ts2.8", "@types/tinycolor2@ts2.8", "@types/tmp@ts2.8", "@types/to-absolute-glob@ts2.8", "@types/tough-cookie@ts2.8", "@types/trim@ts2.8", "@types/tryer@ts2.8", "@types/type-check@ts2.8", "@types/type-is@ts2.8", "@types/ua-parser-js@ts2.8", "@types/uglify-js@ts2.8", "@types/uglifyjs-webpack-plugin@ts2.8", "@types/underscore@ts2.8", "@types/uniq@ts2.8", "@types/uniqid@ts2.8", "@types/untildify@ts2.8", "@types/urijs@ts2.8", "@types/url-join@ts2.8", "@types/url-parse@ts2.8", "@types/url-regex@ts2.8", "@types/user-home@ts2.8", "@types/util-deprecate@ts2.8", "@types/util.promisify@ts2.8", "@types/utils-merge@ts2.8", "@types/uuid@ts2.8", "@types/vali-date@ts2.8", "@types/vary@ts2.8", "@types/verror@ts2.8", "@types/vinyl@ts2.8", "@types/vinyl-fs@ts2.8", "@types/warning@ts2.8", "@types/watch@ts2.8", "@types/watchpack@ts2.8", "@types/webpack-dev-middleware@ts2.8", "@types/webpack-sources@ts2.8", "@types/which@ts2.8", "@types/window-size@ts2.8", "@types/wrap-ansi@ts2.8", "@types/write-file-atomic@ts2.8", "@types/ws@ts2.8", "@types/xml2js@ts2.8", "@types/xmlbuilder@ts2.8", "@types/xtend@ts2.8", "@types/yallist@ts2.8", "@types/yargs@ts2.8", "@types/yauzl@ts2.8", "@types/yeoman-generator@ts2.8", "@types/zen-observable@ts2.8", "@types/react-content-loader@ts2.8"]; const expectedCommands = [ @@ -1704,7 +1704,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: typingsInstaller:: recomputing resolutions of unresolved imports", () => { + describe("unittests:: tsserver:: typingsInstaller:: recomputing resolutions of unresolved imports", () => { const globalTypingsCacheLocation = "/tmp"; const appPath = "/a/b/app.js" as Path; const foooPath = "/a/b/node_modules/fooo/index.d.ts"; @@ -1775,7 +1775,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: typingsInstaller:: tsserver:: with inferred Project", () => { + describe("unittests:: tsserver:: typingsInstaller:: tsserver:: with inferred Project", () => { it("when projectRootPath is provided", () => { const projects = "/users/username/projects"; const projectRootPath = `${projects}/san2`; diff --git a/src/testRunner/unittests/tsserver/untitledFiles.ts b/src/testRunner/unittests/tsserver/untitledFiles.ts index 2c429707a94..845a53eb218 100644 --- a/src/testRunner/unittests/tsserver/untitledFiles.ts +++ b/src/testRunner/unittests/tsserver/untitledFiles.ts @@ -1,5 +1,5 @@ namespace ts.projectSystem { - describe("tsserver:: Untitled files", () => { + describe("unittests:: tsserver:: Untitled files", () => { it("Can convert positions to locations", () => { const aTs: File = { path: "/proj/a.ts", content: "" }; const tsconfig: File = { path: "/proj/tsconfig.json", content: "{}" }; diff --git a/src/testRunner/unittests/tsserver/versionCache.ts b/src/testRunner/unittests/tsserver/versionCache.ts index bd75b2bbaf4..60eaa6ca42e 100644 --- a/src/testRunner/unittests/tsserver/versionCache.ts +++ b/src/testRunner/unittests/tsserver/versionCache.ts @@ -15,7 +15,7 @@ namespace ts { assert.equal(editedText, checkText); } - describe(`tsserver:: VersionCache TS code`, () => { + describe(`unittests:: tsserver:: VersionCache TS code`, () => { let validateEditAtLineCharIndex: (line: number, char: number, deleteLength: number, insertString: string) => void; before(() => { @@ -77,7 +77,7 @@ var q:Point=p;`; }); }); - describe(`tsserver:: VersionCache simple text`, () => { + describe(`unittests:: tsserver:: VersionCache simple text`, () => { let validateEditAtPosition: (position: number, deleteLength: number, insertString: string) => void; let testContent: string; let lines: string[]; @@ -181,7 +181,7 @@ and grew 1cm per day`; }); }); - describe(`tsserver:: VersionCache stress test`, () => { + describe(`unittests:: tsserver:: VersionCache stress test`, () => { let rsa: number[] = []; let la: number[] = []; let las: number[] = []; diff --git a/src/testRunner/unittests/tsserver/watchEnvironment.ts b/src/testRunner/unittests/tsserver/watchEnvironment.ts index 5ca4d6f4570..2c195e0c7d2 100644 --- a/src/testRunner/unittests/tsserver/watchEnvironment.ts +++ b/src/testRunner/unittests/tsserver/watchEnvironment.ts @@ -1,6 +1,6 @@ namespace ts.projectSystem { import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; - describe("tsserver:: watchEnvironment:: tsserverProjectSystem watchDirectories implementation", () => { + describe("unittests:: tsserver:: watchEnvironment:: tsserverProjectSystem watchDirectories implementation", () => { function verifyCompletionListWithNewFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { const projectFolder = "/a/username/project"; const projectSrcFolder = `${projectFolder}/src`; @@ -83,7 +83,7 @@ namespace ts.projectSystem { }); }); - describe("tsserver:: watchEnvironment:: tsserverProjectSystem Watched recursive directories with windows style file system", () => { + describe("unittests:: tsserver:: watchEnvironment:: tsserverProjectSystem Watched recursive directories with windows style file system", () => { function verifyWatchedDirectories(rootedPath: string, useProjectAtRoot: boolean) { const root = useProjectAtRoot ? rootedPath : `${rootedPath}myfolder/allproject/`; const configFile: File = { From f117184562901c18fd642abc15647b1eb1645c0d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 20 Dec 2018 14:00:45 -0800 Subject: [PATCH 19/20] More tsserver tests refactoring --- src/testRunner/tsconfig.json | 6 +- .../unittests/tsserver/compileOnSave.ts | 95 + .../unittests/tsserver/configuredProjects.ts | 1010 +++++ .../events/projectLanguageServiceState.ts | 51 + .../unittests/tsserver/events/surveyReady.ts | 111 + .../unittests/tsserver/externalProjects.ts | 464 +++ .../tsserver/getApplicableRefactors.ts | 12 + .../unittests/tsserver/inferredProjects.ts | 120 + src/testRunner/unittests/tsserver/projects.ts | 1426 +++++++ .../unittests/tsserverProjectSystem.ts | 3281 ----------------- 10 files changed, 3294 insertions(+), 3282 deletions(-) create mode 100644 src/testRunner/unittests/tsserver/configuredProjects.ts create mode 100644 src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts create mode 100644 src/testRunner/unittests/tsserver/events/surveyReady.ts create mode 100644 src/testRunner/unittests/tsserver/getApplicableRefactors.ts create mode 100644 src/testRunner/unittests/tsserver/projects.ts delete mode 100644 src/testRunner/unittests/tsserverProjectSystem.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 4d200c4b8f9..9b5f74cb18c 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -61,7 +61,6 @@ "unittests/tsbuild.ts", "unittests/tsbuildWatchMode.ts", "unittests/tscWatchMode.ts", - "unittests/tsserverProjectSystem.ts", "unittests/config/commandLineParsing.ts", "unittests/config/configurationExtension.ts", "unittests/config/convertCompilerOptionsFromJson.ts", @@ -98,15 +97,19 @@ "unittests/tsserver/compileOnSave.ts", "unittests/tsserver/completions.ts", "unittests/tsserver/configFileSearch.ts", + "unittests/tsserver/configuredProjects.ts", "unittests/tsserver/declarationFileMaps.ts", "unittests/tsserver/documentRegistry.ts", "unittests/tsserver/duplicatePackages.ts", "unittests/tsserver/events/largeFileReferenced.ts", + "unittests/tsserver/events/projectLanguageServiceState.ts", "unittests/tsserver/events/projectLoading.ts", "unittests/tsserver/events/projectUpdatedInBackground.ts", + "unittests/tsserver/events/surveyReady.ts", "unittests/tsserver/externalProjects.ts", "unittests/tsserver/forceConsistentCasingInFileNames.ts", "unittests/tsserver/formatSettings.ts", + "unittests/tsserver/getApplicableRefactors.ts", "unittests/tsserver/getEditsForFileRename.ts", "unittests/tsserver/importHelpers.ts", "unittests/tsserver/inferredProjects.ts", @@ -118,6 +121,7 @@ "unittests/tsserver/openFile.ts", "unittests/tsserver/projectErrors.ts", "unittests/tsserver/projectReferences.ts", + "unittests/tsserver/projects.ts", "unittests/tsserver/refactors.ts", "unittests/tsserver/reload.ts", "unittests/tsserver/rename.ts", diff --git a/src/testRunner/unittests/tsserver/compileOnSave.ts b/src/testRunner/unittests/tsserver/compileOnSave.ts index 667144f6a06..7dc9ea0497c 100644 --- a/src/testRunner/unittests/tsserver/compileOnSave.ts +++ b/src/testRunner/unittests/tsserver/compileOnSave.ts @@ -695,4 +695,99 @@ namespace ts.projectSystem { }); }); + describe("unittests:: tsserver:: compileOnSave:: CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => { + const projectRoot = "/user/username/projects/myproject"; + const core: File = { + path: `${projectRoot}/core/core.ts`, + content: "let z = 10;" + }; + const app1: File = { + path: `${projectRoot}/app1/app.ts`, + content: "let x = 10;" + }; + const app2: File = { + path: `${projectRoot}/app2/app.ts`, + content: "let y = 10;" + }; + const app1Config: File = { + path: `${projectRoot}/app1/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts", "../core/core.ts"], + compilerOptions: { outFile: "build/output.js" }, + compileOnSave: true + }) + }; + const app2Config: File = { + path: `${projectRoot}/app2/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts", "../core/core.ts"], + compilerOptions: { outFile: "build/output.js" }, + compileOnSave: true + }) + }; + const files = [libFile, core, app1, app2, app1Config, app2Config]; + + function insertString(session: TestSession, file: File) { + session.executeCommandSeq({ + command: protocol.CommandTypes.Change, + arguments: { + file: file.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: "let k = 1" + } + }); + } + + function getSession() { + const host = createServerHost(files); + const session = createSession(host); + openFilesForSession([app1, app2, core], session); + const service = session.getProjectService(); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + const project1 = service.configuredProjects.get(app1Config.path)!; + const project2 = service.configuredProjects.get(app2Config.path)!; + checkProjectActualFiles(project1, [libFile.path, app1.path, core.path, app1Config.path]); + checkProjectActualFiles(project2, [libFile.path, app2.path, core.path, app2Config.path]); + insertString(session, app1); + insertString(session, app2); + assert.equal(project1.dirty, true); + assert.equal(project2.dirty, true); + return session; + } + + it("when projectFile is specified", () => { + const session = getSession(); + const response = session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { + file: core.path, + projectFileName: app1Config.path + } + }).response; + assert.deepEqual(response, [ + { projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true } + ]); + assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false); + assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, true); + }); + + it("when projectFile is not specified", () => { + const session = getSession(); + const response = session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { + file: core.path + } + }).response; + assert.deepEqual(response, [ + { projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true }, + { projectFileName: app2Config.path, fileNames: [core.path, app2.path], projectUsesOutFile: true } + ]); + assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false); + assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, false); + }); + }); } diff --git a/src/testRunner/unittests/tsserver/configuredProjects.ts b/src/testRunner/unittests/tsserver/configuredProjects.ts new file mode 100644 index 00000000000..d1525f8dbeb --- /dev/null +++ b/src/testRunner/unittests/tsserver/configuredProjects.ts @@ -0,0 +1,1010 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: ConfiguredProjects", () => { + it("create configured project without file list", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: ` + { + "compilerOptions": {}, + "exclude": [ + "e" + ] + }` + }; + const file1: File = { + path: "/a/b/c/f1.ts", + content: "let x = 1" + }; + const file2: File = { + path: "/a/b/d/f2.ts", + content: "let y = 1" + }; + const file3: File = { + path: "/a/b/e/f3.ts", + content: "let z = 1" + }; + + const host = createServerHost([configFile, libFile, file1, file2, file3]); + const projectService = createProjectService(host); + const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); + + assert(configFileName, "should find config file"); + assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); + checkNumberOfInferredProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = configuredProjectAt(projectService, 0); + checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); + checkProjectRootFiles(project, [file1.path, file2.path]); + // watching all files except one that was open + checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); + const configFileDirectory = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, [configFileDirectory, combinePaths(configFileDirectory, nodeModulesAtTypes)], /*recursive*/ true); + }); + + it("create configured project with the file list", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: ` + { + "compilerOptions": {}, + "include": ["*.ts"] + }` + }; + const file1: File = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2: File = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const file3: File = { + path: "/a/b/c/f3.ts", + content: "let z = 1" + }; + + const host = createServerHost([configFile, libFile, file1, file2, file3]); + const projectService = createProjectService(host); + const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); + + assert(configFileName, "should find config file"); + assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); + checkNumberOfInferredProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = configuredProjectAt(projectService, 0); + checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); + checkProjectRootFiles(project, [file1.path, file2.path]); + // watching all files except one that was open + checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); + checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ false); + }); + + it("add and then remove a config file in a folder with loose files", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "files": ["commonFile1.ts"] + }` + }; + const filesWithoutConfig = [libFile, commonFile1, commonFile2]; + const host = createServerHost(filesWithoutConfig); + + const filesWithConfig = [libFile, commonFile1, commonFile2, configFile]; + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + projectService.openClientFile(commonFile2.path); + + projectService.checkNumberOfProjects({ inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, libFile.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); + + const configFileLocations = ["/", "/a/", "/a/b/"]; + const watchedFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]).concat(libFile.path); + checkWatchedFiles(host, watchedFiles); + + // Add a tsconfig file + host.reloadFS(filesWithConfig); + host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles + + projectService.checkNumberOfProjects({ inferredProjects: 2, configuredProjects: 1 }); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); + checkProjectActualFiles(projectService.configuredProjects.get(configFile.path)!, [libFile.path, commonFile1.path, configFile.path]); + + checkWatchedFiles(host, watchedFiles); + + // remove the tsconfig file + host.reloadFS(filesWithoutConfig); + + projectService.checkNumberOfProjects({ inferredProjects: 2 }); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); + + host.checkTimeoutQueueLengthAndRun(1); // Refresh inferred projects + + projectService.checkNumberOfProjects({ inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, libFile.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); + checkWatchedFiles(host, watchedFiles); + }); + + it("add new files to a configured project without file list", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([commonFile1, libFile, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + const configFileDir = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, [configFileDir, combinePaths(configFileDir, nodeModulesAtTypes)], /*recursive*/ true); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path]); + + // add a new ts file + host.reloadFS([commonFile1, commonFile2, libFile, configFile]); + host.checkTimeoutQueueLengthAndRun(2); + // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + }); + + it("should ignore non-existing files specified in the config file", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": [ + "commonFile1.ts", + "commonFile3.ts" + ] + }` + }; + const host = createServerHost([commonFile1, commonFile2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + projectService.openClientFile(commonFile2.path); + + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path]); + checkNumberOfInferredProjects(projectService, 1); + }); + + it("handle recreated files correctly", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([commonFile1, commonFile2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + + // delete commonFile2 + host.reloadFS([commonFile1, configFile]); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectRootFiles(project, [commonFile1.path]); + + // re-add commonFile2 + host.reloadFS([commonFile1, commonFile2, configFile]); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + }); + + it("files explicitly excluded in config file", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "exclude": ["/a/c"] + }` + }; + const excludedFile1: File = { + path: "/a/c/excluedFile1.ts", + content: `let t = 1;` + }; + + const host = createServerHost([commonFile1, commonFile2, excludedFile1, configFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(commonFile1.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + projectService.openClientFile(excludedFile1.path); + checkNumberOfInferredProjects(projectService, 1); + }); + + it("should properly handle module resolution changes in config file", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: `import { T } from "module1";` + }; + const nodeModuleFile: File = { + path: "/a/b/node_modules/module1.ts", + content: `export interface T {}` + }; + const classicModuleFile: File = { + path: "/a/module1.ts", + content: `export interface T {}` + }; + const randomFile: File = { + path: "/a/file1.ts", + content: `export interface T {}` + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "moduleResolution": "node" + }, + "files": ["${file1.path}"] + }` + }; + const files = [file1, nodeModuleFile, classicModuleFile, configFile, randomFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.openClientFile(nodeModuleFile.path); + projectService.openClientFile(classicModuleFile.path); + + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + const project = configuredProjectAt(projectService, 0); + const inferredProject0 = projectService.inferredProjects[0]; + checkProjectActualFiles(project, [file1.path, nodeModuleFile.path, configFile.path]); + checkProjectActualFiles(projectService.inferredProjects[0], [classicModuleFile.path]); + + configFile.content = `{ + "compilerOptions": { + "moduleResolution": "classic" + }, + "files": ["${file1.path}"] + }`; + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(2); + + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); // will not remove project 1 + checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + const inferredProject1 = projectService.inferredProjects[1]; + checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); + + // Open random file and it will reuse first inferred project + projectService.openClientFile(randomFile.path); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + checkProjectActualFiles(projectService.inferredProjects[0], [randomFile.path]); // Reuses first inferred project + assert.strictEqual(projectService.inferredProjects[1], inferredProject1); + checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); + }); + + it("should keep the configured project when the opened file is referenced by the project but not its root", () => { + const file1: File = { + path: "/a/b/main.ts", + content: "import { objA } from './obj-a';" + }; + const file2: File = { + path: "/a/b/obj-a.ts", + content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts" ] + }` + }; + const host = createServerHost([file1, file2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.closeClientFile(file1.path); + projectService.openClientFile(file2.path); + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + }); + + it("should keep the configured project when the opened file is referenced by the project but not its root", () => { + const file1: File = { + path: "/a/b/main.ts", + content: "import { objA } from './obj-a';" + }; + const file2: File = { + path: "/a/b/obj-a.ts", + content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts" ] + }` + }; + const host = createServerHost([file1, file2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.closeClientFile(file1.path); + projectService.openClientFile(file2.path); + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + }); + + it("should tolerate config file errors and still try to build a project", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6", + "allowAnything": true + }, + "someOtherProperty": {} + }` + }; + const host = createServerHost([commonFile1, commonFile2, libFile, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + checkNumberOfConfiguredProjects(projectService, 1); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [commonFile1.path, commonFile2.path]); + }); + + it("should reuse same project if file is opened from the configured project that has no open files", () => { + const file1 = { + path: "/a/b/main.ts", + content: "let x =1;" + }; + const file2 = { + path: "/a/b/main2.ts", + content: "let y =1;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts", "main2.ts" ] + }` + }; + const host = createServerHost([file1, file2, configFile, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(project.hasOpenRef()); // file1 + + projectService.closeClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + 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.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", () => { + const file1 = { + path: "/a/b/main.ts", + content: "let x =1;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts" ] + }` + }; + const host = createServerHost([file1, configFile, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + 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.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("open file become a part of configured project if it is referenced from root file", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "export let x = 5" + }; + const file2 = { + path: "/a/c/f2.ts", + content: `import {x} from "../b/f1"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: "export let y = 1" + }; + const configFile = { + path: "/a/c/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) + }; + + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + + host.reloadFS([file1, file2, file3, configFile]); + host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, file3.path, configFile.path]); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + assert.isTrue(projectService.inferredProjects[1].isOrphan()); + }); + + it("can correctly update configured project when set of root files has changed (new file on disk)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + + const host = createServerHost([file1, configFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); + + host.reloadFS([file1, file2, configFile]); + + host.checkTimeoutQueueLengthAndRun(2); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); + }); + + it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) + }; + + const host = createServerHost([file1, file2, configFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); + + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + + host.reloadFS([file1, file2, modifiedConfigFile]); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); + }); + + it("can update configured project when set of root files was not changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + + const host = createServerHost([file1, file2, configFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, configFile.path]); + + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }) + }; + + host.reloadFS([file1, file2, modifiedConfigFile]); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); + }); + + it("Open ref of configured project when open file gets added to the project as part of configured file update", () => { + const file1: File = { + path: "/a/b/src/file1.ts", + content: "let x = 1;" + }; + const file2: File = { + path: "/a/b/src/file2.ts", + content: "let y = 1;" + }; + const file3: File = { + path: "/a/b/file3.ts", + content: "let z = 1;" + }; + const file4: File = { + 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.isTrue(configProject1.hasOpenRef()); // file1 and file3 + 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(/*hasOpenRef*/ true, 2); // file1, file2, file3 + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + const inferredProject3 = projectService.inferredProjects[1]; + checkProjectActualFiles(inferredProject3, [file4.path]); + assert.strictEqual(inferredProject3, inferredProject2); + + projectService.closeClientFile(file1.path); + projectService.closeClientFile(file2.path); + projectService.closeClientFile(file4.path); + + verifyScriptInfos(); + checkOpenFiles(projectService, [file3]); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file3 + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + assert.isTrue(projectService.inferredProjects[1].isOrphan()); + + projectService.openClientFile(file4.path); + verifyScriptInfos(); + checkOpenFiles(projectService, [file3, file4]); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 1); // file3 + const inferredProject4 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject4, [file4.path]); + + projectService.closeClientFile(file3.path); + verifyScriptInfos(); + checkOpenFiles(projectService, [file4]); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ false, 1); // No open files + const inferredProject5 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject4, [file4.path]); + assert.strictEqual(inferredProject5, inferredProject4); + + const file5: File = { + 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]); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file5.path]); + + function verifyScriptInfos() { + infos.forEach(info => assert.strictEqual(projectService.getScriptInfoForPath(info.path), info)); + } + + function verifyScriptInfosAreUndefined(files: File[]) { + for (const file of files) { + assert.isUndefined(projectService.getScriptInfoForPath(file.path as Path)); + } + } + + function verifyConfiguredProjectStateAfterUpdate(hasOpenRef: boolean, inferredProjects: number) { + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects }); + const configProject2 = projectService.configuredProjects.get(configFile.path)!; + assert.strictEqual(configProject2, configProject1); + checkProjectActualFiles(configProject2, [file1.path, file2.path, file3.path, configFile.path]); + 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: File = { + path: "/a/b/src/file1.ts", + content: "let x = 1;" + }; + const file2: File = { + path: "/a/b/src/file2.ts", + content: "let y = 1;" + }; + const file3: File = { + path: "/a/b/file3.ts", + content: "let z = 1;" + }; + const file4: File = { + 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: 2 }); + 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], inferredProject1); + assert.isTrue(inferredProject1.isOrphan()); + assert.strictEqual(projectService.inferredProjects[1], inferredProject2); + checkProjectActualFiles(inferredProject2, [file4.path]); + }); + + it("files are properly detached when language service is disabled", () => { + const f1 = { + path: "/a/app.js", + content: "var x = 1" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const f3 = { + path: "/a/lib.js", + content: "var x = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true } }) + }; + const host = createServerHost([f1, f2, f3, config]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => + filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + + const projectService = createProjectService(host); + 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.isFalse(project.hasOpenRef()); // No files + assert.isFalse(project.isClosed()); + + for (const f of [f1, f2, f3]) { + // 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("syntactic features work even if language service is disabled", () => { + const f1 = { + path: "/a/app.js", + content: "let x = 1;" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const config = { + path: "/a/jsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, f2, config]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => + filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + const { session, events } = createSessionWithEventTracking(host, server.ProjectLanguageServiceStateEvent); + session.executeCommand({ + seq: 0, + type: "request", + command: "open", + arguments: { file: f1.path } + }); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = configuredProjectAt(projectService, 0); + assert.isFalse(project.languageServiceEnabled, "Language service enabled"); + assert.equal(events.length, 1, "should receive event"); + assert.equal(events[0].data.project, project, "project name"); + assert.isFalse(events[0].data.languageServiceEnabled, "Language service state"); + + const options = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath); + const edits = project.getLanguageService().getFormattingEditsForDocument(f1.path, options); + assert.deepEqual(edits, [{ span: createTextSpan(/*start*/ 7, /*length*/ 3), newText: " " }]); + }); + }); + + describe("unittests:: tsserver:: ConfiguredProjects:: non-existing directories listed in config file input array", () => { + it("should be tolerated without crashing the server", () => { + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "include": ["app/*", "test/**/*", "something"] + }` + }; + const file1 = { + path: "/a/b/file1.ts", + content: "let t = 10;" + }; + + const host = createServerHost([file1, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + host.runQueuedTimeoutCallbacks(); + // Since there is no file open from configFile it would be closed + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 1); + + const inferredProject = projectService.inferredProjects[0]; + assert.isTrue(inferredProject.containsFile(file1.path)); + }); + + it("should be able to handle @types if input file list is empty", () => { + const f = { + path: "/a/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compiler: {}, + files: [] + }) + }; + const t1 = { + path: "/a/node_modules/@types/typings/index.d.ts", + content: `export * from "./lib"` + }; + const t2 = { + path: "/a/node_modules/@types/typings/lib.d.ts", + content: `export const x: number` + }; + const host = createServerHost([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) }); + const projectService = createProjectService(host); + + projectService.openClientFile(f.path); + // Since no file from the configured project is open, it would be closed immediately + projectService.checkNumberOfProjects({ configuredProjects: 0, inferredProjects: 1 }); + }); + + it("should tolerate invalid include files that start in subDirectory", () => { + const projectFolder = "/user/username/projects/myproject"; + const f = { + path: `${projectFolder}/src/server/index.ts`, + content: "let x = 1" + }; + const config = { + path: `${projectFolder}/src/server/tsconfig.json`, + content: JSON.stringify({ + compiler: { + module: "commonjs", + outDir: "../../build" + }, + include: [ + "../src/**/*.ts" + ] + }) + }; + const host = createServerHost([f, config, libFile], { useCaseSensitiveFileNames: true }); + const projectService = createProjectService(host); + + projectService.openClientFile(f.path); + // Since no file from the configured project is open, it would be closed immediately + projectService.checkNumberOfProjects({ configuredProjects: 0, inferredProjects: 1 }); + }); + + it("Changed module resolution reflected when specifying files list", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: 'import classc from "file2"' + }; + const file2a: File = { + path: "/a/file2.ts", + content: "export classc { method2a() { return 10; } }" + }; + const file2: File = { + path: "/a/b/file2.ts", + content: "export classc { method2() { return 10; } }" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: [file1.path], compilerOptions: { module: "amd" } }) + }; + const files = [file1, file2a, configFile, libFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isDefined(project); + checkProjectActualFiles(project, map(files, file => file.path)); + checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); + checkWatchedDirectoriesDetailed(host, ["/a/b"], 1, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); + + files.push(file2); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); + checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); + + // On next file open the files file2a should be closed and not watched any more + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); + }); + + it("Failed lookup locations uses parent most node_modules directory", () => { + const root = "/user/username/rootfolder"; + const file1: File = { + path: "/a/b/src/file1.ts", + content: 'import { classc } from "module1"' + }; + const module1: File = { + path: "/a/b/node_modules/module1/index.d.ts", + content: `import { class2 } from "module2"; + export classc { method2a(): class2; }` + }; + const module2: File = { + path: "/a/b/node_modules/module2/index.d.ts", + content: "export class2 { method2() { return 10; } }" + }; + const module3: File = { + path: "/a/b/node_modules/module/node_modules/module3/index.d.ts", + content: "export class3 { method2() { return 10; } }" + }; + const configFile: File = { + path: "/a/b/src/tsconfig.json", + content: JSON.stringify({ files: ["file1.ts"] }) + }; + 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); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isDefined(project); + checkProjectActualFiles(project, [file1.path, libFile.path, module1.path, module2.path, configFile.path]); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, [], /*recursive*/ false); + const watchedRecursiveDirectories = getTypeRootsFromLocation(root + "/a/b/src"); + watchedRecursiveDirectories.push(`${root}/a/b/src/node_modules`, `${root}/a/b/node_modules`); + checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts b/src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts new file mode 100644 index 00000000000..231c46c350b --- /dev/null +++ b/src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts @@ -0,0 +1,51 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: events:: ProjectLanguageServiceStateEvent", () => { + it("language service disabled events are triggered", () => { + const f1 = { + path: "/a/app.js", + content: "let x = 1;" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const config = { + path: "/a/jsconfig.json", + content: "{}" + }; + const configWithExclude = { + path: config.path, + content: JSON.stringify({ exclude: ["largefile.js"] }) + }; + const host = createServerHost([f1, f2, config]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => + filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + + const { session, events } = createSessionWithEventTracking(host, server.ProjectLanguageServiceStateEvent); + session.executeCommand({ + seq: 0, + type: "request", + command: "open", + arguments: { file: f1.path } + }); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = configuredProjectAt(projectService, 0); + assert.isFalse(project.languageServiceEnabled, "Language service enabled"); + assert.equal(events.length, 1, "should receive event"); + assert.equal(events[0].data.project, project, "project name"); + assert.equal(events[0].data.project.getProjectName(), config.path, "config path"); + assert.isFalse(events[0].data.languageServiceEnabled, "Language service state"); + + host.reloadFS([f1, f2, configWithExclude]); + host.checkTimeoutQueueLengthAndRun(2); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.isTrue(project.languageServiceEnabled, "Language service enabled"); + assert.equal(events.length, 2, "should receive event"); + assert.equal(events[1].data.project, project, "project"); + assert.equal(events[1].data.project.getProjectName(), config.path, "config path"); + assert.isTrue(events[1].data.languageServiceEnabled, "Language service state"); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/events/surveyReady.ts b/src/testRunner/unittests/tsserver/events/surveyReady.ts new file mode 100644 index 00000000000..b04746800d0 --- /dev/null +++ b/src/testRunner/unittests/tsserver/events/surveyReady.ts @@ -0,0 +1,111 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: events:: SurveyReady", () => { + function createSessionWithEventHandler(host: TestServerHost) { + const { session, events: surveyEvents } = createSessionWithEventTracking(host, server.SurveyReady); + + return { session, verifySurveyReadyEvent }; + + function verifySurveyReadyEvent(numberOfEvents: number) { + assert.equal(surveyEvents.length, numberOfEvents); + const expectedEvents = numberOfEvents === 0 ? [] : [{ + eventName: server.SurveyReady, + data: { surveyId: "checkJs" } + }]; + assert.deepEqual(surveyEvents, expectedEvents); + } + } + + it("doesn't log an event when checkJs isn't set", () => { + const projectRoot = "/user/username/projects/project"; + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {} }), + }; + const host = createServerHost([file, tsconfig]); + const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); + const service = session.getProjectService(); + openFilesForSession([file], session); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const project = service.configuredProjects.get(tsconfig.path)!; + checkProjectActualFiles(project, [file.path, tsconfig.path]); + + verifySurveyReadyEvent(0); + }); + + it("logs an event when checkJs is set", () => { + const projectRoot = "/user/username/projects/project"; + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { checkJs: true } }), + }; + const host = createServerHost([file, tsconfig]); + const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); + openFilesForSession([file], session); + + verifySurveyReadyEvent(1); + }); + + it("logs an event when checkJs is set, only the first time", () => { + const projectRoot = "/user/username/projects/project"; + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const rando: File = { + path: `/rando/calrissian.ts`, + content: "export function f() { }" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { checkJs: true } }), + }; + const host = createServerHost([file, tsconfig]); + const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); + openFilesForSession([file], session); + + verifySurveyReadyEvent(1); + + closeFilesForSession([file], session); + openFilesForSession([rando], session); + openFilesForSession([file], session); + + verifySurveyReadyEvent(1); + }); + + it("logs an event when checkJs is set after closing and reopening", () => { + const projectRoot = "/user/username/projects/project"; + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const rando: File = { + path: `/rando/calrissian.ts`, + content: "export function f() { }" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({}), + }; + const host = createServerHost([file, tsconfig]); + const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); + openFilesForSession([file], session); + + verifySurveyReadyEvent(0); + + closeFilesForSession([file], session); + openFilesForSession([rando], session); + host.writeFile(tsconfig.path, JSON.stringify({ compilerOptions: { checkJs: true } })); + openFilesForSession([file], session); + + verifySurveyReadyEvent(1); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/externalProjects.ts b/src/testRunner/unittests/tsserver/externalProjects.ts index 87615b24d12..12a97151f4e 100644 --- a/src/testRunner/unittests/tsserver/externalProjects.ts +++ b/src/testRunner/unittests/tsserver/externalProjects.ts @@ -1,5 +1,469 @@ namespace ts.projectSystem { describe("unittests:: tsserver:: ExternalProjects", () => { + describe("can handle tsconfig file name with difference casing", () => { + function verifyConfigFileCasing(lazyConfiguredProjectsFromExternalProject: boolean) { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + include: [] + }) + }; + + const host = createServerHost([f1, config], { useCaseSensitiveFileNames: false }); + const service = createProjectService(host); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); + service.openExternalProject({ + projectFileName: "/a/b/project.csproj", + rootFiles: toExternalFiles([f1.path, upperCaseConfigFilePath]), + options: {} + }); + service.checkNumberOfProjects({ configuredProjects: 1 }); + const project = service.configuredProjects.get(config.path)!; + if (lazyConfiguredProjectsFromExternalProject) { + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded + checkProjectActualFiles(project, emptyArray); + } + else { + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded + checkProjectActualFiles(project, [upperCaseConfigFilePath]); + } + + service.openClientFile(f1.path); + service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); + + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project is updated + checkProjectActualFiles(project, [upperCaseConfigFilePath]); + checkProjectActualFiles(service.inferredProjects[0], [f1.path]); + } + + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ false); + }); + + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ true); + }); + }); + + it("remove not-listed external projects", () => { + const f1 = { + path: "/a/app.ts", + content: "let x = 1" + }; + const f2 = { + path: "/b/app.ts", + content: "let x = 1" + }; + const f3 = { + path: "/c/app.ts", + content: "let x = 1" + }; + const makeProject = (f: File) => ({ projectFileName: f.path + ".csproj", rootFiles: [toExternalFile(f.path)], options: {} }); + const p1 = makeProject(f1); + const p2 = makeProject(f2); + const p3 = makeProject(f3); + + const host = createServerHost([f1, f2, f3]); + const session = createSession(host); + + session.executeCommand({ + seq: 1, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p1, p2] } + }); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { externalProjects: 2 }); + assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); + assert.equal(projectService.externalProjects[1].getProjectName(), p2.projectFileName); + + session.executeCommand({ + seq: 2, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p1, p3] } + }); + checkNumberOfProjects(projectService, { externalProjects: 2 }); + assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); + assert.equal(projectService.externalProjects[1].getProjectName(), p3.projectFileName); + + session.executeCommand({ + seq: 3, + type: "request", + command: "openExternalProjects", + arguments: { projects: [] } + }); + checkNumberOfProjects(projectService, { externalProjects: 0 }); + + session.executeCommand({ + seq: 3, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p2] } + }); + assert.equal(projectService.externalProjects[0].getProjectName(), p2.projectFileName); + }); + + it("should not close external project with no open files", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y =1;" + }; + const externalProjectName = "externalproject"; + const host = createServerHost([file1, file2]); + const projectService = createProjectService(host); + projectService.openExternalProject({ + rootFiles: toExternalFiles([file1.path, file2.path]), + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + // open client file - should not lead to creation of inferred project + projectService.openClientFile(file1.path, file1.content); + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + // close client file - external project should still exists + projectService.closeClientFile(file1.path); + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + projectService.closeExternalProject(externalProjectName); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 0); + }); + + it("external project for dynamic file", () => { + const externalProjectName = "^ScriptDocument1 file1.ts"; + const externalFiles = toExternalFiles(["^ScriptDocument1 file1.ts"]); + const host = createServerHost([]); + const projectService = createProjectService(host); + projectService.openExternalProject({ + rootFiles: externalFiles, + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + externalFiles[0].content = "let x =1;"; + projectService.applyChangesInOpenFiles(externalFiles, [], []); + }); + + it("external project that included config files", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: {}, + files: ["f1.ts"] + } + ) + }; + const file2 = { + path: "/a/c/f2.ts", + content: "let y =1;" + }; + const config2 = { + path: "/a/c/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: {}, + files: ["f2.ts"] + } + ) + }; + const file3 = { + path: "/a/d/f3.ts", + content: "let z =1;" + }; + const externalProjectName = "externalproject"; + const host = createServerHost([file1, file2, file3, config1, config2]); + const projectService = createProjectService(host); + projectService.openExternalProject({ + rootFiles: toExternalFiles([config1.path, config2.path, file3.path]), + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfProjects(projectService, { configuredProjects: 2 }); + const proj1 = projectService.configuredProjects.get(config1.path); + const proj2 = projectService.configuredProjects.get(config2.path); + assert.isDefined(proj1); + assert.isDefined(proj2); + + // open client file - should not lead to creation of inferred project + projectService.openClientFile(file1.path, file1.content); + checkNumberOfProjects(projectService, { configuredProjects: 2 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); + + projectService.openClientFile(file3.path, file3.content); + checkNumberOfProjects(projectService, { configuredProjects: 2, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); + + projectService.closeExternalProject(externalProjectName); + // open file 'file1' from configured project keeps project alive + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.isUndefined(projectService.configuredProjects.get(config2.path)); + + projectService.closeClientFile(file3.path); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.isUndefined(projectService.configuredProjects.get(config2.path)); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + + projectService.closeClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.isUndefined(projectService.configuredProjects.get(config2.path)); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + + projectService.openClientFile(file2.path, file2.content); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(config1.path)); + assert.isDefined(projectService.configuredProjects.get(config2.path)); + }); + + it("external project with included config file opened after configured project", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const externalProjectName = "externalproject"; + const host = createServerHost([file1, configFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + projectService.openExternalProject({ + rootFiles: toExternalFiles([configFile.path]), + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + projectService.closeClientFile(file1.path); + // configured project is alive since it is opened as part of external project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + projectService.closeExternalProject(externalProjectName); + checkNumberOfProjects(projectService, { configuredProjects: 0 }); + }); + + it("external project with included config file opened after configured project and then closed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/f2.ts", + content: "let x = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const externalProjectName = "externalproject"; + const host = createServerHost([file1, file2, libFile, configFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path); + + projectService.openExternalProject({ + rootFiles: toExternalFiles([configFile.path]), + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + + projectService.closeExternalProject(externalProjectName); + // configured project is alive since file is still open + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + + projectService.closeClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(configFile.path)); + }); + + it("can correctly update external project when set of root files has changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const host = createServerHost([file1, file2]); + const projectService = createProjectService(host); + + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path]) }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [file1.path]); + + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]) }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + }); + + it("can update external project when set of root files was not changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "m"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: "export let y = 1" + }; + const file3 = { + path: "/a/m.ts", + content: "export let y = 1" + }; + + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + + projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.NodeJs }, rootFiles: toExternalFiles([file1.path, file2.path]) }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path]); + + projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.Classic }, rootFiles: toExternalFiles([file1.path, file2.path]) }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path, file3.path]); + }); + + it("language service disabled state is updated in external projects", () => { + const f1 = { + path: "/a/app.js", + content: "var x = 1" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const host = createServerHost([f1, f2]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => + filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + + const service = createProjectService(host); + const projectFileName = "/a/proj.csproj"; + + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 1"); + + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path]), + options: {} + }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isTrue(service.externalProjects[0].languageServiceEnabled, "language service should be enabled"); + + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 2"); + }); + + describe("deleting config file opened from the external project works", () => { + function verifyDeletingConfigFile(lazyConfiguredProjectsFromExternalProject: boolean) { + const site = { + path: "/user/someuser/project/js/site.js", + content: "" + }; + const configFile = { + path: "/user/someuser/project/tsconfig.json", + content: "{}" + }; + const projectFileName = "/user/someuser/project/WebApplication6.csproj"; + const host = createServerHost([libFile, site, configFile]); + const projectService = createProjectService(host); + projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + + const externalProject: protocol.ExternalProject = { + projectFileName, + rootFiles: [toExternalFile(site.path), toExternalFile(configFile.path)], + options: { allowJs: false }, + typeAcquisition: { include: [] } + }; + + projectService.openExternalProjects([externalProject]); + + let knownProjects = projectService.synchronizeProjectList([]); + checkNumberOfProjects(projectService, { configuredProjects: 1, externalProjects: 0, inferredProjects: 0 }); + + const configProject = configuredProjectAt(projectService, 0); + checkProjectActualFiles(configProject, lazyConfiguredProjectsFromExternalProject ? + emptyArray : // Since no files opened from this project, its not loaded + [configFile.path]); + + host.reloadFS([libFile, site]); + host.checkTimeoutQueueLengthAndRun(1); + + knownProjects = projectService.synchronizeProjectList(map(knownProjects, proj => proj.info!)); // TODO: GH#18217 GH#20039 + checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 0, inferredProjects: 0 }); + + externalProject.rootFiles.length = 1; + projectService.openExternalProjects([externalProject]); + + checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 1, inferredProjects: 0 }); + checkProjectActualFiles(projectService.externalProjects[0], [site.path, libFile.path]); + } + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ false); + }); + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ true); + }); + }); + describe("correctly handling add/remove tsconfig - 1", () => { function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { const f1 = { diff --git a/src/testRunner/unittests/tsserver/getApplicableRefactors.ts b/src/testRunner/unittests/tsserver/getApplicableRefactors.ts new file mode 100644 index 00000000000..1f9576e1d63 --- /dev/null +++ b/src/testRunner/unittests/tsserver/getApplicableRefactors.ts @@ -0,0 +1,12 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: getApplicableRefactors", () => { + it("works when taking position", () => { + const aTs: File = { path: "/a.ts", content: "" }; + const session = createSession(createServerHost([aTs])); + openFilesForSession([aTs], session); + const response = executeSessionRequest( + session, protocol.CommandTypes.GetApplicableRefactors, { file: aTs.path, line: 1, offset: 1 }); + assert.deepEqual | undefined>(response, []); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/inferredProjects.ts b/src/testRunner/unittests/tsserver/inferredProjects.ts index 47e871efb82..9d7ccaa1224 100644 --- a/src/testRunner/unittests/tsserver/inferredProjects.ts +++ b/src/testRunner/unittests/tsserver/inferredProjects.ts @@ -1,5 +1,125 @@ namespace ts.projectSystem { describe("unittests:: tsserver:: Inferred projects", () => { + it("create inferred project", () => { + const appFile: File = { + path: "/a/b/c/app.ts", + content: ` + import {f} from "./module" + console.log(f) + ` + }; + + const moduleFile: File = { + path: "/a/b/c/module.d.ts", + content: `export let x: number` + }; + const host = createServerHost([appFile, moduleFile, libFile]); + const projectService = createProjectService(host); + const { configFileName } = projectService.openClientFile(appFile.path); + + assert(!configFileName, `should not find config, got: '${configFileName}`); + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 1); + + const project = projectService.inferredProjects[0]; + + checkArray("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); + const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; + const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); + checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); + checkWatchedDirectories(host, ["/a/b/c"], /*recursive*/ false); + checkWatchedDirectories(host, [combinePaths(getDirectoryPath(appFile.path), nodeModulesAtTypes)], /*recursive*/ true); + }); + + it("should use only one inferred project if 'useOneInferredProject' is set", () => { + const file1 = { + path: "/a/b/main.ts", + content: "let x =1;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts" ] + }` + }; + const file2 = { + path: "/a/c/main.ts", + content: "let x =1;" + }; + + const file3 = { + path: "/a/d/main.ts", + content: "let x =1;" + }; + + const host = createServerHost([file1, file2, file3, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + projectService.openClientFile(file3.path); + + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 1); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path, libFile.path]); + + + host.reloadFS([file1, configFile, file2, file3, libFile]); + host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 1); + checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); + }); + + it("disable inferred project", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + + const host = createServerHost([file1]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); + + projectService.openClientFile(file1.path, file1.content); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const proj = projectService.inferredProjects[0]; + assert.isDefined(proj); + + assert.isFalse(proj.languageServiceEnabled); + }); + + it("project settings for inferred projects", () => { + const file1 = { + path: "/a/b/app.ts", + content: `import {x} from "mod"` + }; + const modFile = { + path: "/a/mod.ts", + content: "export let x: number" + }; + const host = createServerHost([file1, modFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.openClientFile(modFile.path); + + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + const inferredProjects = projectService.inferredProjects.slice(); + checkProjectActualFiles(inferredProjects[0], [file1.path]); + checkProjectActualFiles(inferredProjects[1], [modFile.path]); + + projectService.setCompilerOptionsForInferredProjects({ moduleResolution: ModuleResolutionKind.Classic }); + host.checkTimeoutQueueLengthAndRun(3); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); + checkProjectActualFiles(inferredProjects[0], [file1.path, modFile.path]); + assert.isTrue(inferredProjects[1].isOrphan()); + }); + it("should support files without extensions", () => { const f = { path: "/a/compile", diff --git a/src/testRunner/unittests/tsserver/projects.ts b/src/testRunner/unittests/tsserver/projects.ts new file mode 100644 index 00000000000..00941a1d3c9 --- /dev/null +++ b/src/testRunner/unittests/tsserver/projects.ts @@ -0,0 +1,1426 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: Projects", () => { + it("handles the missing files - that were added to program because they were added with /// { + const file1: File = { + path: "/a/b/commonFile1.ts", + content: `/// + let x = y` + }; + const host = createServerHost([file1, libFile]); + const session = createSession(host); + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + + checkNumberOfInferredProjects(projectService, 1); + const project = projectService.inferredProjects[0]; + checkProjectRootFiles(project, [file1.path]); + checkProjectActualFiles(project, [file1.path, libFile.path]); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + + // Two errors: CommonFile2 not found and cannot find name y + 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] } + ]); + + host.reloadFS([file1, commonFile2, libFile]); + host.runQueuedTimeoutCallbacks(); + checkNumberOfInferredProjects(projectService, 1); + assert.strictEqual(projectService.inferredProjects[0], project, "Inferred project should be same"); + checkProjectRootFiles(project, [file1.path]); + checkProjectActualFiles(project, [file1.path, libFile.path, commonFile2.path]); + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + }); + + it("should create new inferred projects for files excluded from a configured project", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}", "${commonFile2.path}"] + }` + }; + const files = [commonFile1, commonFile2, configFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + configFile.content = `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}"] + }`; + host.reloadFS(files); + + checkNumberOfConfiguredProjects(projectService, 1); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects + checkNumberOfConfiguredProjects(projectService, 1); + checkProjectRootFiles(project, [commonFile1.path]); + + projectService.openClientFile(commonFile2.path); + checkNumberOfInferredProjects(projectService, 1); + }); + + it("should disable features when the files are too large", () => { + const file1 = { + path: "/a/b/f1.js", + content: "let x =1;", + fileSize: 10 * 1024 * 1024 + }; + const file2 = { + path: "/a/b/f2.js", + content: "let y =1;", + fileSize: 6 * 1024 * 1024 + }; + const file3 = { + path: "/a/b/f3.js", + content: "let y =1;", + fileSize: 6 * 1024 * 1024 + }; + + const proj1name = "proj1", proj2name = "proj2", proj3name = "proj3"; + + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + + projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path]), options: {}, projectFileName: proj1name }); + const proj1 = projectService.findProject(proj1name)!; + assert.isTrue(proj1.languageServiceEnabled); + + projectService.openExternalProject({ rootFiles: toExternalFiles([file2.path]), options: {}, projectFileName: proj2name }); + const proj2 = projectService.findProject(proj2name)!; + assert.isTrue(proj2.languageServiceEnabled); + + projectService.openExternalProject({ rootFiles: toExternalFiles([file3.path]), options: {}, projectFileName: proj3name }); + const proj3 = projectService.findProject(proj3name)!; + assert.isFalse(proj3.languageServiceEnabled); + }); + + describe("ignoreConfigFiles", () => { + it("external project including config file", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: {}, + files: ["f1.ts"] + } + ) + }; + + const externalProjectName = "externalproject"; + const host = createServerHost([file1, config1]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); + projectService.openExternalProject({ + rootFiles: toExternalFiles([file1.path, config1.path]), + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const proj = projectService.externalProjects[0]; + assert.isDefined(proj); + + assert.isTrue(proj.fileExists(file1.path)); + }); + + it("loose file included in config file (openClientFile)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: {}, + files: ["f1.ts"] + } + ) + }; + + const host = createServerHost([file1, config1]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); + projectService.openClientFile(file1.path, file1.content); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const proj = projectService.inferredProjects[0]; + assert.isDefined(proj); + + assert.isTrue(proj.fileExists(file1.path)); + }); + + it("loose file included in config file (applyCodeChanges)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: {}, + files: ["f1.ts"] + } + ) + }; + + const host = createServerHost([file1, config1]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); + projectService.applyChangesInOpenFiles([{ fileName: file1.path, content: file1.content }], [], []); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const proj = projectService.inferredProjects[0]; + assert.isDefined(proj); + + assert.isTrue(proj.fileExists(file1.path)); + }); + }); + + it("reload regular file after closing", () => { + const f1 = { + path: "/a/b/app.ts", + content: "x." + }; + const f2 = { + path: "/a/b/lib.ts", + content: "let x: number;" + }; + + const host = createServerHost([f1, f2, libFile]); + const service = createProjectService(host); + service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: toExternalFiles([f1.path, f2.path]), options: {} }); + + service.openClientFile(f1.path); + service.openClientFile(f2.path, "let x: string"); + + service.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); + + const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!; + // should contain completions for string + assert.isTrue(completions1.entries.some(e => e.name === "charAt"), "should contain 'charAt'"); + assert.isFalse(completions1.entries.some(e => e.name === "toExponential"), "should not contain 'toExponential'"); + + service.closeClientFile(f2.path); + const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!; + // should contain completions for string + assert.isFalse(completions2.entries.some(e => e.name === "charAt"), "should not contain 'charAt'"); + assert.isTrue(completions2.entries.some(e => e.name === "toExponential"), "should contain 'toExponential'"); + }); + + it("clear mixed content file after closing", () => { + const f1 = { + path: "/a/b/app.ts", + content: " " + }; + const f2 = { + path: "/a/b/lib.html", + content: "" + }; + + const host = createServerHost([f1, f2, libFile]); + const service = createProjectService(host); + service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: [{ fileName: f1.path }, { fileName: f2.path, hasMixedContent: true }], options: {} }); + + service.openClientFile(f1.path); + service.openClientFile(f2.path, "let somelongname: string"); + + service.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); + + const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!; + assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'"); + + service.closeClientFile(f2.path); + const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!; + assert.isFalse(completions2.entries.some(e => e.name === "somelongname"), "should not contain 'somelongname'"); + const sf2 = service.externalProjects[0].getLanguageService().getProgram()!.getSourceFile(f2.path)!; + assert.equal(sf2.text, ""); + }); + + it("changes in closed files are reflected in project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export let x = 1` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const inferredProject0 = projectService.inferredProjects[0]; + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); + + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); + const inferredProject1 = projectService.inferredProjects[1]; + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + + const modifiedFile2 = { + path: file2.path, + content: `export * from "../c/f3"` // now inferred project should inclule file3 + }; + + host.reloadFS([file1, modifiedFile2, file3]); + host.checkTimeoutQueueLengthAndRun(2); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, modifiedFile2.path, file3.path]); + assert.strictEqual(projectService.inferredProjects[1], inferredProject1); + assert.isTrue(inferredProject1.isOrphan()); + }); + + it("deleted files affect project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export * from "../c/f3"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); + + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + + host.reloadFS([file1, file3]); + host.checkTimeoutQueueLengthAndRun(2); + + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + }); + + it("ignores files excluded by a custom safe type list", () => { + const file1 = { + path: "/a/b/f1.js", + content: "export let x = 5" + }; + const office = { + path: "/lib/duckquack-3.min.js", + content: "whoa do @@ not parse me ok thanks!!!" + }; + const host = createServerHost([file1, office, customTypesMap]); + const projectService = createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, office.path]) }); + const proj = projectService.externalProjects[0]; + assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); + assert.deepEqual(proj.getTypeAcquisition().include, ["duck-types"]); + } finally { + projectService.resetSafeList(); + } + }); + + it("file with name constructor.js doesnt cause issue with typeAcquisition when safe type list", () => { + const file1 = { + path: "/a/b/f1.js", + content: `export let x = 5; import { s } from "s"` + }; + const constructorFile = { + path: "/a/b/constructor.js", + content: "const x = 10;" + }; + const bliss = { + path: "/a/b/bliss.js", + content: "export function is() { return true; }" + }; + const host = createServerHost([file1, libFile, constructorFile, bliss, customTypesMap]); + let request: string | undefined; + const cachePath = "/a/data"; + const typingsInstaller: server.ITypingsInstaller = { + isKnownTypesPackageName: returnFalse, + installPackage: notImplemented, + inspectValue: notImplemented, + enqueueInstallTypingsRequest: (proj, typeAcquisition, unresolvedImports) => { + assert.isUndefined(request); + request = JSON.stringify(server.createInstallTypingsRequest(proj, typeAcquisition, unresolvedImports || server.emptyArray, cachePath)); + }, + attach: noop, + onProjectClosed: noop, + globalTypingsCacheLocation: cachePath + }; + + const projectName = "project"; + const projectService = createProjectService(host, { typingsInstaller }); + projectService.openExternalProject({ projectFileName: projectName, options: {}, rootFiles: toExternalFiles([file1.path, constructorFile.path, bliss.path]) }); + assert.equal(request, JSON.stringify({ + projectName, + fileNames: [libFile.path, file1.path, constructorFile.path, bliss.path], + compilerOptions: { allowNonTsExtensions: true, noEmitForJsFiles: true }, + typeAcquisition: { include: ["blissfuljs"], exclude: [], enable: true }, + unresolvedImports: ["s"], + projectRootPath: "/", + cachePath, + kind: "discover" + })); + const response = JSON.parse(request!); + request = undefined; + projectService.updateTypingsForProject({ + kind: "action::set", + projectName: response.projectName, + typeAcquisition: response.typeAcquisition, + compilerOptions: response.compilerOptions, + typings: emptyArray, + unresolvedImports: response.unresolvedImports, + }); + + host.checkTimeoutQueueLengthAndRun(2); + assert.isUndefined(request); + }); + + it("ignores files excluded by the default type list", () => { + const file1 = { + path: "/a/b/f1.js", + content: "export let x = 5" + }; + const minFile = { + path: "/c/moment.min.js", + content: "unspecified" + }; + const kendoFile1 = { + path: "/q/lib/kendo/kendo.all.min.js", + content: "unspecified" + }; + const kendoFile2 = { + path: "/q/lib/kendo/kendo.ui.min.js", + content: "unspecified" + }; + const kendoFile3 = { + path: "/q/lib/kendo-ui/kendo.all.js", + content: "unspecified" + }; + const officeFile1 = { + path: "/scripts/Office/1/excel-15.debug.js", + content: "unspecified" + }; + const officeFile2 = { + path: "/scripts/Office/1/powerpoint.js", + content: "unspecified" + }; + const files = [file1, minFile, kendoFile1, kendoFile2, kendoFile3, officeFile1, officeFile2]; + const host = createServerHost(files); + const projectService = createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles(files.map(f => f.path)) }); + const proj = projectService.externalProjects[0]; + assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); + assert.deepEqual(proj.getTypeAcquisition().include, ["kendo-ui", "office"]); + } finally { + projectService.resetSafeList(); + } + }); + + it("removes version numbers correctly", () => { + const testData: [string, string][] = [ + ["jquery-max", "jquery-max"], + ["jquery.min", "jquery"], + ["jquery-min.4.2.3", "jquery"], + ["jquery.min.4.2.1", "jquery"], + ["minimum", "minimum"], + ["min", "min"], + ["min.3.2", "min"], + ["jquery", "jquery"] + ]; + for (const t of testData) { + assert.equal(removeMinAndVersionNumbers(t[0]), t[1], t[0]); + } + }); + + it("ignores files excluded by a legacy safe type list", () => { + const file1 = { + path: "/a/b/bliss.js", + content: "let x = 5" + }; + const file2 = { + path: "/a/b/foo.js", + content: "" + }; + const file3 = { + path: "/a/b/Bacon.js", + content: "let y = 5" + }; + const host = createServerHost([file1, file2, file3, customTypesMap]); + const projectService = createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]), typeAcquisition: { enable: true } }); + const proj = projectService.externalProjects[0]; + assert.deepEqual(proj.getFileNames(), [file2.path]); + } finally { + projectService.resetSafeList(); + } + }); + + it("correctly migrate files between projects", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` + export * from "../c/f2"; + export * from "../d/f3";` + }; + const file2 = { + path: "/a/c/f2.ts", + content: "export let x = 1;" + }; + const file3 = { + path: "/a/d/f3.ts", + content: "export let y = 1;" + }; + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); + let inferredProjects = projectService.inferredProjects.slice(); + + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + inferredProjects = projectService.inferredProjects.slice(); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[1]); + checkProjectRootFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); + inferredProjects = projectService.inferredProjects.slice(); + + projectService.closeClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 3 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); + inferredProjects = projectService.inferredProjects.slice(); + + projectService.closeClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 3 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); + assert.strictEqual(projectService.inferredProjects[2], inferredProjects[2]); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + assert.isTrue(projectService.inferredProjects[2].isOrphan()); + + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[2]); + assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); + checkProjectActualFiles(projectService.inferredProjects[0], [file3.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + }); + + it("regression test for crash in acquireOrUpdateDocument", () => { + const tsFile = { + fileName: "/a/b/file1.ts", + path: "/a/b/file1.ts", + content: "" + }; + const jsFile = { + path: "/a/b/file1.js", + content: "var x = 10;", + fileName: "/a/b/file1.js", + scriptKind: "JS" as "JS" + }; + + const host = createServerHost([]); + const projectService = createProjectService(host); + projectService.applyChangesInOpenFiles([tsFile], [], []); + const projs = projectService.synchronizeProjectList([]); + projectService.findProject(projs[0].info!.projectName)!.getLanguageService().getNavigationBarItems(tsFile.fileName); + projectService.synchronizeProjectList([projs[0].info!]); + projectService.applyChangesInOpenFiles([jsFile], [], []); + }); + + it("config file is deleted", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1;" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 2;" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const host = createServerHost([file1, file2, config]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + host.reloadFS([file1, file2]); + host.checkTimeoutQueueLengthAndRun(1); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + }); + + it("loading files with correct priority", () => { + const f1 = { + path: "/a/main.ts", + content: "let x = 1" + }; + const f2 = { + path: "/a/main.js", + content: "var y = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { allowJs: true } + }) + }; + const host = createServerHost([f1, f2, config]); + const projectService = createProjectService(host); + projectService.setHostConfiguration({ + extraFileExtensions: [ + { extension: ".js", isMixedContent: false }, + { extension: ".html", isMixedContent: true } + ] + }); + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]); + + // Should close configured project with next file open + projectService.closeClientFile(f1.path); + + projectService.openClientFile(f2.path); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(config.path)); + checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]); + }); + + it("tsconfig script block support", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` ` + }; + const file2 = { + path: "/a/b/f2.html", + content: `var hello = "hello";` + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true } }) + }; + const host = createServerHost([file1, file2, config]); + const session = createSession(host); + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + + // HTML file will not be included in any projects yet + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const configuredProj = configuredProjectAt(projectService, 0); + checkProjectActualFiles(configuredProj, [file1.path, config.path]); + + // Specify .html extension as mixed content + const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; + const configureHostRequest = makeSessionRequest(CommandNames.Configure, { extraFileExtensions }); + session.executeCommand(configureHostRequest); + + // The configured project should now be updated to include html file + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(configuredProjectAt(projectService, 0), configuredProj, "Same configured project should be updated"); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + // Open HTML file + projectService.applyChangesInOpenFiles( + /*openFiles*/[{ fileName: file2.path, hasMixedContent: true, scriptKind: ScriptKind.JS, content: `var hello = "hello";` }], + /*changedFiles*/ undefined, + /*closedFiles*/ undefined); + + // Now HTML file is included in the project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + // Check identifiers defined in HTML content are available in .ts file + const project = configuredProjectAt(projectService, 0); + let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, emptyOptions); + assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`); + + // Close HTML file + projectService.applyChangesInOpenFiles( + /*openFiles*/ undefined, + /*changedFiles*/ undefined, + /*closedFiles*/[file2.path]); + + // HTML file is still included in project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + // Check identifiers defined in HTML content are not available in .ts file + completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, emptyOptions); + assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`); + }); + + it("no tsconfig script block diagnostic errors", () => { + + // #1. Ensure no diagnostic errors when allowJs is true + const file1 = { + path: "/a/b/f1.ts", + content: ` ` + }; + const file2 = { + path: "/a/b/f2.html", + content: `var hello = "hello";` + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true } }) + }; + + let host = createServerHost([file1, file2, config1, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + let session = createSession(host); + + // Specify .html extension as mixed content in a configure host request + const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; + const configureHostRequest = makeSessionRequest(CommandNames.Configure, { extraFileExtensions }); + session.executeCommand(configureHostRequest); + + openFilesForSession([file1], session); + let projectService = session.getProjectService(); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + let diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + + // #2. Ensure no errors when allowJs is false + const config2 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: false } }) + }; + + host = createServerHost([file1, file2, config2, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + session = createSession(host); + + session.executeCommand(configureHostRequest); + + openFilesForSession([file1], session); + projectService = session.getProjectService(); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + + // #3. Ensure no errors when compiler options aren't specified + const config3 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({}) + }; + + host = createServerHost([file1, file2, config3, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + session = createSession(host); + + session.executeCommand(configureHostRequest); + + openFilesForSession([file1], session); + projectService = session.getProjectService(); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + + // #4. Ensure no errors when files are explicitly specified in tsconfig + const config4 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true }, files: [file1.path, file2.path] }) + }; + + host = createServerHost([file1, file2, config4, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + session = createSession(host); + + session.executeCommand(configureHostRequest); + + openFilesForSession([file1], session); + projectService = session.getProjectService(); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + + // #4. Ensure no errors when files are explicitly excluded in tsconfig + const config5 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true }, exclude: [file2.path] }) + }; + + host = createServerHost([file1, file2, config5, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + session = createSession(host); + + session.executeCommand(configureHostRequest); + + openFilesForSession([file1], session); + projectService = session.getProjectService(); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + }); + + it("project structure update is deferred if files are not added\removed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `import {x} from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: "export let x = 1" + }; + const host = createServerHost([file1, file2]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + projectService.applyChangesInOpenFiles( + /*openFiles*/ undefined, + /*changedFiles*/[{ fileName: file1.path, changes: [{ span: createTextSpan(0, file1.path.length), newText: "let y = 1" }] }], + /*closedFiles*/ undefined); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + projectService.ensureInferredProjectsUpToDate_TestOnly(); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + }); + + it("files with mixed content are handled correctly", () => { + const file1 = { + path: "/a/b/f1.html", + content: `