From 3bc41784f08e3d808040bab59870f4fcbd562fc8 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 12 Sep 2023 11:52:49 -0700 Subject: [PATCH] Fix issue with wildcard with supported extensions when plugins add external files and override getScriptKind to add script kinds for the additional extensions (#55716) --- src/compiler/watchUtilities.ts | 32 ++- src/server/editorServices.ts | 1 + src/server/project.ts | 2 +- src/testRunner/unittests/tsserver/plugins.ts | 69 +++++++ ...on-ts-extensions-with-wildcard-matching.js | 185 ++++++++++++++++++ 5 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 tests/baselines/reference/tsserver/plugins/new-files-with-non-ts-extensions-with-wildcard-matching.js diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 7073986dd59..422ec2f70be 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -20,9 +20,11 @@ import { FileWatcherCallback, FileWatcherEventKind, find, + getAllowJSCompilerOption, getBaseFileName, getDirectoryPath, getNormalizedAbsolutePath, + getResolveJsonModule, hasExtension, identity, insertSorted, @@ -44,6 +46,7 @@ import { removeIgnoredPath, returnNoopFileWatcher, returnTrue, + ScriptKind, setSysLog, SortedArray, SortedReadonlyArray, @@ -563,6 +566,7 @@ export interface IsIgnoredFileFromWildCardWatchingInput { useCaseSensitiveFileNames: boolean; writeLog: (s: string) => void; toPath: (fileName: string) => Path; + getScriptKind?: (fileName: string) => ScriptKind; } /** @internal */ export function isIgnoredFileFromWildCardWatching({ @@ -577,6 +581,7 @@ export function isIgnoredFileFromWildCardWatching({ useCaseSensitiveFileNames, writeLog, toPath, + getScriptKind, }: IsIgnoredFileFromWildCardWatchingInput): boolean { const newPath = removeIgnoredPath(fileOrDirectoryPath); if (!newPath) { @@ -588,8 +593,12 @@ export function isIgnoredFileFromWildCardWatching({ if (fileOrDirectoryPath === watchedDirPath) return false; // If the the added or created file or directory is not supported file name, ignore the file - // But when watched directory is added/removed, we need to reload the file list - if (hasExtension(fileOrDirectoryPath) && !isSupportedSourceFileName(fileOrDirectory, options, extraFileExtensions)) { + if ( + hasExtension(fileOrDirectoryPath) && !( + isSupportedSourceFileName(fileOrDirectory, options, extraFileExtensions) || + isSupportedScriptKind() + ) + ) { writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrDirectory}`); return true; } @@ -634,6 +643,25 @@ export function isIgnoredFileFromWildCardWatching({ builderProgram.getState().fileInfos.has(file) : !!find(program as readonly string[], rootFile => toPath(rootFile) === file); } + + function isSupportedScriptKind() { + if (!getScriptKind) return false; + const scriptKind = getScriptKind(fileOrDirectory); + switch (scriptKind) { + case ScriptKind.TS: + case ScriptKind.TSX: + case ScriptKind.Deferred: + case ScriptKind.External: + return true; + case ScriptKind.JS: + case ScriptKind.JSX: + return getAllowJSCompilerOption(options); + case ScriptKind.JSON: + return getResolveJsonModule(options); + case ScriptKind.Unknown: + return false; + } + } } function isBuilderProgram(program: Program | T): program is T { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index ab02f3548d4..b9779f3d700 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1526,6 +1526,7 @@ export class ProjectService { useCaseSensitiveFileNames: this.host.useCaseSensitiveFileNames, writeLog: s => this.logger.info(s), toPath: s => this.toPath(s), + getScriptKind: configuredProjectForConfig ? (fileName => configuredProjectForConfig.getScriptKind(fileName)) : undefined, }) ) return; diff --git a/src/server/project.ts b/src/server/project.ts index 8df08a36cd7..489154dd0b4 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -663,7 +663,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo } getScriptKind(fileName: string) { - const info = this.getOrCreateScriptInfoAndAttachToProject(fileName); + const info = this.projectService.getScriptInfoForPath(this.toPath(fileName)); return (info && info.scriptKind)!; // TODO: GH#18217 } diff --git a/src/testRunner/unittests/tsserver/plugins.ts b/src/testRunner/unittests/tsserver/plugins.ts index 503c0702d16..bcc1add3b97 100644 --- a/src/testRunner/unittests/tsserver/plugins.ts +++ b/src/testRunner/unittests/tsserver/plugins.ts @@ -218,3 +218,72 @@ describe("unittests:: tsserver:: plugins:: overriding getSupportedCodeFixes", () baselineTsserverLogs("plugins", "getSupportedCodeFixes can be proxied", session); }); }); + +describe("unittests:: tsserver:: plugins:: supportedExtensions::", () => { + it("new files with non ts extensions and wildcard matching", () => { + 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: "bVue file", + }; + const config: File = { + path: "/user/username/projects/myproject/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: { composite: true }, + include: ["*.ts", "*.vue"], + }, + undefined, + " ", + ), + }; + const host = createServerHost([aTs, bVue, config, libFile]); + 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 => + ts.fileExtensionIs(fileName, ".vue") ? + ts.ScriptKind.TS : + originalScriptKind(fileName); + const originalGetScriptSnapshot = info.languageServiceHost.getScriptSnapshot.bind(info.languageServiceHost); + info.languageServiceHost.getScriptSnapshot = fileName => + ts.fileExtensionIs(fileName, ".vue") ? + ts.ScriptSnapshot.fromString(`export const y = "${info.languageServiceHost.readFile(fileName)}";`) : + originalGetScriptSnapshot(fileName); + return proxy; + }, + getExternalFiles: (project: ts.server.Project) => { + if (project.projectKind !== ts.server.ProjectKind.Configured) return []; + 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()); + return parsed.fileNames; + }, + }), + error: undefined, + }; + }; + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host), globalPlugins: ["myplugin"] }); + openFilesForSession([aTs], session); + + host.writeFile("/user/username/projects/myproject/c.vue", "cVue file"); + host.runQueuedTimeoutCallbacks(); + + baselineTsserverLogs("plugins", "new files with non ts extensions with wildcard matching", session); + }); +}); diff --git a/tests/baselines/reference/tsserver/plugins/new-files-with-non-ts-extensions-with-wildcard-matching.js b/tests/baselines/reference/tsserver/plugins/new-files-with-non-ts-extensions-with-wildcard-matching.js new file mode 100644 index 00000000000..d8ff142f281 --- /dev/null +++ b/tests/baselines/reference/tsserver/plugins/new-files-with-non-ts-extensions-with-wildcard-matching.js @@ -0,0 +1,185 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/user/username/projects/myproject/a.ts] +export const a = 10; + +//// [/user/username/projects/myproject/b.vue] +bVue file + +//// [/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/a.ts" + }, + "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/a.ts :: 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] 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 +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/b.vue 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/@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 +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /user/username/projects/myproject/tsconfig.json Version: 1 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 SVC-1-0 "export const a = 10;" + /user/username/projects/myproject/b.vue Text-1 "export const y = \"bVue file\";" + + + ../../../../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] 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/a.ts 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/@types: *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/b.vue: *new* + {} +/user/username/projects/myproject/tsconfig.json: *new* + {} + +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Triggered with /user/username/projects/myproject/c.vue :: WatchInfo: /user/username/projects/myproject 0 undefined Config: /user/username/projects/myproject/tsconfig.json WatchType: Wild card directory +Info seq [hh:mm:ss:mss] Scheduled: /user/username/projects/myproject/tsconfig.json +Info seq [hh:mm:ss:mss] Scheduled: *ensureProjectForOpenFiles* +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Triggered with /user/username/projects/myproject/c.vue :: WatchInfo: /user/username/projects/myproject 0 undefined Config: /user/username/projects/myproject/tsconfig.json WatchType: Wild card directory +Before running Timeout callback:: count: 2 +1: /user/username/projects/myproject/tsconfig.json +2: *ensureProjectForOpenFiles* +//// [/user/username/projects/myproject/c.vue] +cVue file + + +Info seq [hh:mm:ss:mss] Running: /user/username/projects/myproject/tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/c.vue 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] Finishing updateGraphWorker: Project: /user/username/projects/myproject/tsconfig.json Version: 2 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 (4) + /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 SVC-1-0 "export const a = 10;" + /user/username/projects/myproject/b.vue Text-1 "export const y = \"bVue file\";" + /user/username/projects/myproject/c.vue Text-1 "export const y = \"cVue file\";" + + + ../../../../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' + c.vue + Matched by include pattern '*.vue' in 'tsconfig.json' + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Running: *ensureProjectForOpenFiles* +Info seq [hh:mm:ss:mss] Before ensureProjectForOpenFiles: +Info seq [hh:mm:ss:mss] Project '/user/username/projects/myproject/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (4) + +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/a.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /user/username/projects/myproject/tsconfig.json +Info seq [hh:mm:ss:mss] After ensureProjectForOpenFiles: +Info seq [hh:mm:ss:mss] Project '/user/username/projects/myproject/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (4) + +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/a.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /user/username/projects/myproject/tsconfig.json +After running Timeout callback:: count: 0 + +PolledWatches:: +/user/username/projects/myproject/node_modules/@types: + {"pollingInterval":500} +/user/username/projects/node_modules/@types: + {"pollingInterval":500} + +FsWatches:: +/a/lib/lib.d.ts: + {} +/user/username/projects/myproject: + {} +/user/username/projects/myproject/b.vue: + {} +/user/username/projects/myproject/c.vue: *new* + {} +/user/username/projects/myproject/tsconfig.json: + {}