From 353ccb7688351ae33ccf6e0acb913aa30621eaf4 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 4 Mar 2024 15:56:35 -0800 Subject: [PATCH] Ensure correct script kind and text when using cached sourceFile from scriptInfo (#57641) --- src/harness/incrementalUtils.ts | 7 +- src/services/documentRegistry.ts | 3 +- src/testRunner/unittests/tsserver/plugins.ts | 120 ++++-- ...criptKind-changes-for-the-external-file.js | 386 ++++++++++++++++++ 4 files changed, 487 insertions(+), 29 deletions(-) create mode 100644 tests/baselines/reference/tsserver/plugins/when-scriptKind-changes-for-the-external-file.js diff --git a/src/harness/incrementalUtils.ts b/src/harness/incrementalUtils.ts index dc14cf88f8c..8a361aa4a9f 100644 --- a/src/harness/incrementalUtils.ts +++ b/src/harness/incrementalUtils.ts @@ -436,14 +436,17 @@ function verifyProgram(service: ts.server.ProjectService, project: ts.server.Pro compilerHost.readFile = fileName => { const path = project.toPath(fileName); const info = project.projectService.filenameToScriptInfo.get(path); - if (info?.isDynamicOrHasMixedContent() || project.fileIsOpen(path)) { - return ts.getSnapshotText(info!.getSnapshot()); + if (info?.isDynamicOrHasMixedContent()) { + return ts.getSnapshotText(info.getSnapshot()); } if (!ts.isAnySupportedFileExtension(path)) { // Some external file const snapshot = project.getScriptSnapshot(path); return snapshot ? ts.getSnapshotText(snapshot) : undefined; } + if (project.fileIsOpen(path)) { + return ts.getSnapshotText(info!.getSnapshot()); + } // Read only rooted disk paths from host similar to ProjectService if (!ts.isRootedDiskPath(fileName) || !compilerHost.fileExists(fileName)) return undefined; if (ts.hasTSFileExtension(fileName)) return readFile(fileName); diff --git a/src/services/documentRegistry.ts b/src/services/documentRegistry.ts index e1061a8fc9d..fc42cacce7d 100644 --- a/src/services/documentRegistry.ts +++ b/src/services/documentRegistry.ts @@ -13,6 +13,7 @@ import { getKeyForCompilerOptions, getOrUpdate, getSetExternalModuleIndicator, + getSnapshotText, identity, IScriptSnapshot, isDeclarationFileName, @@ -300,7 +301,7 @@ export function createDocumentRegistryInternal(useCaseSensitiveFileNames?: boole let entry = bucketEntry && getDocumentRegistryEntry(bucketEntry, scriptKind); if (!entry && externalCache) { const sourceFile = externalCache.getDocument(keyWithMode, path); - if (sourceFile) { + if (sourceFile && sourceFile.scriptKind === scriptKind && sourceFile.text === getSnapshotText(scriptSnapshot)) { Debug.assert(acquiring); entry = { sourceFile, diff --git a/src/testRunner/unittests/tsserver/plugins.ts b/src/testRunner/unittests/tsserver/plugins.ts index 3a6f755f860..d5985a0212d 100644 --- a/src/testRunner/unittests/tsserver/plugins.ts +++ b/src/testRunner/unittests/tsserver/plugins.ts @@ -7,6 +7,7 @@ import { baselineTsserverLogs, openFilesForSession, TestSession, + verifyGetErrRequest, } from "../helpers/tsserver"; import { createServerHost, @@ -282,6 +283,35 @@ describe("unittests:: tsserver:: plugins:: overriding getSupportedCodeFixes", () }); describe("unittests:: tsserver:: plugins:: supportedExtensions::", () => { + function createGetExternalFiles(getSession: () => TestSession) { + const externalFiles = new Map(); + return (project: ts.server.Project, updateLevel: ts.ProgramUpdateLevel) => { + if (project.projectKind !== ts.server.ProjectKind.Configured) return []; + if (updateLevel === ts.ProgramUpdateLevel.Update) { + const existing = externalFiles.get(project); + if (existing) { + getSession().logger.log(`getExternalFiles:: Returning cached .vue files`); + return existing; + } + } + getSession().logger.log(`getExternalFiles:: Getting new list of .vue files`); + const configFile = project.getProjectName(); + const config = ts.readJsonConfigFile(configFile, project.readFile.bind(project)); + const parseHost: ts.ParseConfigHost = { + useCaseSensitiveFileNames: project.useCaseSensitiveFileNames(), + fileExists: project.fileExists.bind(project), + readFile: project.readFile.bind(project), + readDirectory: (...args) => { + args[1] = [".vue"]; + return project.readDirectory(...args); + }, + }; + const parsed = ts.parseJsonSourceFileConfigFileContent(config, parseHost, project.getCurrentDirectory()); + externalFiles.set(project, parsed.fileNames); + return parsed.fileNames; + }; + } + it("new files with non ts extensions and wildcard matching", () => { const aTs: File = { path: "/user/username/projects/myproject/a.ts", @@ -303,7 +333,6 @@ describe("unittests:: tsserver:: plugins:: supportedExtensions::", () => { }), }; const host = createServerHost([aTs, dTs, bVue, config, libFile]); - const externalFiles = new Map(); host.require = () => { return { module: () => ({ @@ -321,31 +350,7 @@ describe("unittests:: tsserver:: plugins:: supportedExtensions::", () => { originalGetScriptSnapshot(fileName); return proxy; }, - getExternalFiles: (project: ts.server.Project, updateLevel: ts.ProgramUpdateLevel) => { - if (project.projectKind !== ts.server.ProjectKind.Configured) return []; - if (updateLevel === ts.ProgramUpdateLevel.Update) { - const existing = externalFiles.get(project); - if (existing) { - session.logger.log(`getExternalFiles:: Returning cached .vue files`); - return existing; - } - } - session.logger.log(`getExternalFiles:: Getting new list of .vue files`); - const configFile = project.getProjectName(); - const config = ts.readJsonConfigFile(configFile, project.readFile.bind(project)); - const parseHost: ts.ParseConfigHost = { - useCaseSensitiveFileNames: project.useCaseSensitiveFileNames(), - fileExists: project.fileExists.bind(project), - readFile: project.readFile.bind(project), - readDirectory: (...args) => { - args[1] = [".vue"]; - return project.readDirectory(...args); - }, - }; - const parsed = ts.parseJsonSourceFileConfigFileContent(config, parseHost, project.getCurrentDirectory()); - externalFiles.set(project, parsed.fileNames); - return parsed.fileNames; - }, + getExternalFiles: createGetExternalFiles(() => session), }), error: undefined, }; @@ -361,4 +366,67 @@ describe("unittests:: tsserver:: plugins:: supportedExtensions::", () => { baselineTsserverLogs("plugins", "new files with non ts extensions with wildcard matching", session); }); + + it("when scriptKind changes for the external file", () => { + const aTs: File = { + path: "/user/username/projects/myproject/a.ts", + content: `export const a = 10;`, + }; + const bVue: File = { + path: "/user/username/projects/myproject/b.vue", + content: "bVueFile", + }; + const config: File = { + path: "/user/username/projects/myproject/tsconfig.json", + content: jsonToReadableText({ + compilerOptions: { composite: true }, + include: ["*.ts", "*.vue"], + }), + }; + const host = createServerHost([aTs, bVue, config, libFile]); + let currentVueScriptKind = ts.ScriptKind.TS; + host.require = () => { + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + const proxy = Harness.LanguageService.makeDefaultProxy(info); + const originalScriptKind = info.languageServiceHost.getScriptKind!.bind(info.languageServiceHost); + info.languageServiceHost.getScriptKind = fileName => + fileName === bVue.path ? + currentVueScriptKind : + originalScriptKind(fileName); + const originalGetScriptSnapshot = info.languageServiceHost.getScriptSnapshot.bind(info.languageServiceHost); + info.languageServiceHost.getScriptSnapshot = fileName => + fileName === bVue.path ? + ts.ScriptSnapshot.fromString(`import { y } from "${bVue.content}";`) : // Change the text so that imports change and we need to reconstruct program + originalGetScriptSnapshot(fileName); + return proxy; + }, + getExternalFiles: createGetExternalFiles(() => session), + }), + error: undefined, + }; + }; + const session = new TestSession({ host, globalPlugins: ["myplugin"] }); + openFilesForSession([bVue], session); + + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ + fileName: bVue.path, + textChanges: [{ + start: { line: 1, offset: bVue.content.length + 1 }, + end: { line: 1, offset: bVue.content.length + 1 }, + newText: "Updated", + }], + }], + }, + }); + bVue.content += "Updated"; + currentVueScriptKind = ts.ScriptKind.JS; + + verifyGetErrRequest({ session, files: [bVue] }); + baselineTsserverLogs("plugins", "when scriptKind changes for the external file", session); + }); }); diff --git a/tests/baselines/reference/tsserver/plugins/when-scriptKind-changes-for-the-external-file.js b/tests/baselines/reference/tsserver/plugins/when-scriptKind-changes-for-the-external-file.js new file mode 100644 index 00000000000..6e9d27e1cdb --- /dev/null +++ b/tests/baselines/reference/tsserver/plugins/when-scriptKind-changes-for-the-external-file.js @@ -0,0 +1,386 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info seq [hh:mm:ss:mss] Provided types map file "/typesMap.json" doesn't exist +Before request +//// [/user/username/projects/myproject/a.ts] +export const a = 10; + +//// [/user/username/projects/myproject/b.vue] +bVueFile + +//// [/user/username/projects/myproject/tsconfig.json] +{ + "compilerOptions": { + "composite": true + }, + "include": [ + "*.ts", + "*.vue" + ] +} + +//// [/a/lib/lib.d.ts] +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } + + +Info seq [hh:mm:ss:mss] request: + { + "command": "open", + "arguments": { + "file": "/user/username/projects/myproject/b.vue" + }, + "seq": 1, + "type": "request" + } +Info seq [hh:mm:ss:mss] Search path: /user/username/projects/myproject +Info seq [hh:mm:ss:mss] For info: /user/username/projects/myproject/b.vue :: Config file name: /user/username/projects/myproject/tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /user/username/projects/myproject/tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/tsconfig.json 2000 undefined Project: /user/username/projects/myproject/tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "projectLoadingStart", + "body": { + "projectName": "/user/username/projects/myproject/tsconfig.json", + "reason": "Creating possible configured project for /user/username/projects/myproject/b.vue to open" + } + } +Info seq [hh:mm:ss:mss] Config: /user/username/projects/myproject/tsconfig.json : { + "rootNames": [ + "/user/username/projects/myproject/a.ts" + ], + "options": { + "composite": true, + "configFilePath": "/user/username/projects/myproject/tsconfig.json" + } +} +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject 0 undefined Config: /user/username/projects/myproject/tsconfig.json WatchType: Wild card directory +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject 0 undefined Config: /user/username/projects/myproject/tsconfig.json WatchType: Wild card directory +Info seq [hh:mm:ss:mss] Loading global plugin myplugin +Info seq [hh:mm:ss:mss] Enabling plugin myplugin from candidate paths: /a/lib/tsc.js/../../.. +Info seq [hh:mm:ss:mss] Loading myplugin from /a/lib/tsc.js/../../.. (resolved to /a/lib/tsc.js/../../../node_modules) +Info seq [hh:mm:ss:mss] Plugin validation succeeded +getExternalFiles:: Getting new list of .vue files +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/a.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /user/username/projects/myproject/tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules 1 undefined Project: /user/username/projects/myproject/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules 1 undefined Project: /user/username/projects/myproject/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules 1 undefined Project: /user/username/projects/myproject/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules 1 undefined Project: /user/username/projects/myproject/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 undefined Project: /user/username/projects/myproject/tsconfig.json WatchType: Type roots +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 undefined Project: /user/username/projects/myproject/tsconfig.json WatchType: Type roots +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 undefined Project: /user/username/projects/myproject/tsconfig.json WatchType: Type roots +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 undefined Project: /user/username/projects/myproject/tsconfig.json WatchType: Type roots +getExternalFiles:: Returning cached .vue files +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /user/username/projects/myproject/tsconfig.json projectStateVersion: 1 projectProgramVersion: 0 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/user/username/projects/myproject/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (3) + /a/lib/lib.d.ts Text-1 "/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }" + /user/username/projects/myproject/a.ts Text-1 "export const a = 10;" + /user/username/projects/myproject/b.vue SVC-1-0 "import { y } from \"bVueFile\";" + + + ../../../../a/lib/lib.d.ts + Default library for target 'es5' + a.ts + Matched by include pattern '*.ts' in 'tsconfig.json' + b.vue + Matched by include pattern '*.vue' in 'tsconfig.json' + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "projectLoadingFinish", + "body": { + "projectName": "/user/username/projects/myproject/tsconfig.json" + } + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "telemetry", + "body": { + "telemetryEventName": "projectInfo", + "payload": { + "projectId": "4a33d78ee40d836c4f4e64c59aed976628aea0013be9585c5ff171dfc41baf98", + "fileStats": { + "js": 0, + "jsSize": 0, + "jsx": 0, + "jsxSize": 0, + "ts": 1, + "tsSize": 20, + "tsx": 0, + "tsxSize": 0, + "dts": 1, + "dtsSize": 334, + "deferred": 0, + "deferredSize": 0 + }, + "compilerOptions": { + "composite": true + }, + "typeAcquisition": { + "enable": false, + "include": false, + "exclude": false + }, + "extends": false, + "files": false, + "include": true, + "exclude": false, + "compileOnSave": false, + "configFileName": "tsconfig.json", + "projectType": "configured", + "languageServiceEnabled": true, + "version": "FakeVersion" + } + } + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "configFileDiag", + "body": { + "triggerFile": "/user/username/projects/myproject/b.vue", + "configFile": "/user/username/projects/myproject/tsconfig.json", + "diagnostics": [] + } + } +Info seq [hh:mm:ss:mss] Search path: /user/username/projects/myproject +Info seq [hh:mm:ss:mss] For info: /user/username/projects/myproject/tsconfig.json :: No config files found. +Info seq [hh:mm:ss:mss] Project '/user/username/projects/myproject/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (3) + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /user/username/projects/myproject/b.vue ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /user/username/projects/myproject/tsconfig.json +Info seq [hh:mm:ss:mss] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/user/username/projects/myproject/node_modules: *new* + {"pollingInterval":500} +/user/username/projects/myproject/node_modules/@types: *new* + {"pollingInterval":500} +/user/username/projects/node_modules: *new* + {"pollingInterval":500} +/user/username/projects/node_modules/@types: *new* + {"pollingInterval":500} + +FsWatches:: +/a/lib/lib.d.ts: *new* + {} +/user/username/projects/myproject: *new* + {} +/user/username/projects/myproject/a.ts: *new* + {} +/user/username/projects/myproject/tsconfig.json: *new* + {} + +Projects:: +/user/username/projects/myproject/tsconfig.json (Configured) *new* + projectStateVersion: 1 + projectProgramVersion: 1 + +ScriptInfos:: +/a/lib/lib.d.ts *new* + version: Text-1 + containingProjects: 1 + /user/username/projects/myproject/tsconfig.json +/user/username/projects/myproject/a.ts *new* + version: Text-1 + containingProjects: 1 + /user/username/projects/myproject/tsconfig.json +/user/username/projects/myproject/b.vue (Open) *new* + version: SVC-1-0 + containingProjects: 1 + /user/username/projects/myproject/tsconfig.json *default* + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "updateOpen", + "arguments": { + "changedFiles": [ + { + "fileName": "/user/username/projects/myproject/b.vue", + "textChanges": [ + { + "start": { + "line": 1, + "offset": 9 + }, + "end": { + "line": 1, + "offset": 9 + }, + "newText": "Updated" + } + ] + } + ] + }, + "seq": 2, + "type": "request" + } +Info seq [hh:mm:ss:mss] response: + { + "response": true, + "responseRequired": true + } +After request + +Projects:: +/user/username/projects/myproject/tsconfig.json (Configured) *changed* + projectStateVersion: 2 *changed* + projectProgramVersion: 1 + dirty: true *changed* + +ScriptInfos:: +/a/lib/lib.d.ts + version: Text-1 + containingProjects: 1 + /user/username/projects/myproject/tsconfig.json +/user/username/projects/myproject/a.ts + version: Text-1 + containingProjects: 1 + /user/username/projects/myproject/tsconfig.json +/user/username/projects/myproject/b.vue (Open) *changed* + version: SVC-1-1 *changed* + containingProjects: 1 + /user/username/projects/myproject/tsconfig.json *default* + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "geterr", + "arguments": { + "delay": 0, + "files": [ + "/user/username/projects/myproject/b.vue" + ] + }, + "seq": 3, + "type": "request" + } +Info seq [hh:mm:ss:mss] response: + { + "responseRequired": false + } +After request + +Timeout callback:: count: 1 +1: checkOne *new* + +Before running Timeout callback:: count: 1 +1: checkOne + +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /user/username/projects/myproject/tsconfig.json +getExternalFiles:: Returning cached .vue files +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /user/username/projects/myproject/tsconfig.json projectStateVersion: 2 projectProgramVersion: 1 structureChanged: true structureIsReused:: SafeModules Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/user/username/projects/myproject/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (3) + /a/lib/lib.d.ts Text-1 "/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }" + /user/username/projects/myproject/a.ts Text-1 "export const a = 10;" + /user/username/projects/myproject/b.vue SVC-1-1 "import { y } from \"bVueFileUpdated\";" + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "syntaxDiag", + "body": { + "file": "/user/username/projects/myproject/b.vue", + "diagnostics": [] + } + } +After running Timeout callback:: count: 0 + +Immedidate callback:: count: 1 +1: semanticCheck *new* + +Projects:: +/user/username/projects/myproject/tsconfig.json (Configured) *changed* + projectStateVersion: 2 + projectProgramVersion: 2 *changed* + dirty: false *changed* + +Before running Immedidate callback:: count: 1 +1: semanticCheck + +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "semanticDiag", + "body": { + "file": "/user/username/projects/myproject/b.vue", + "diagnostics": [] + } + } +After running Immedidate callback:: count: 1 + +Immedidate callback:: count: 1 +2: suggestionCheck *new* + +Before running Immedidate callback:: count: 1 +2: suggestionCheck + +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "suggestionDiag", + "body": { + "file": "/user/username/projects/myproject/b.vue", + "diagnostics": [ + { + "start": { + "line": 1, + "offset": 1 + }, + "end": { + "line": 1, + "offset": 16 + }, + "text": "'y' is declared but its value is never read.", + "code": 6133, + "category": "suggestion", + "reportsUnnecessary": true + } + ] + } + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "requestCompleted", + "body": { + "request_seq": 3 + } + } +After running Immedidate callback:: count: 0