Ensure correct script kind and text when using cached sourceFile from scriptInfo (#57641)

This commit is contained in:
Sheetal Nandi
2024-03-04 15:56:35 -08:00
committed by GitHub
parent 0f6f7c308a
commit 353ccb7688
4 changed files with 487 additions and 29 deletions

View File

@@ -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);

View File

@@ -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,

View File

@@ -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<ts.server.Project, string[]>();
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<ts.server.Project, string[]>();
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<ts.server.protocol.UpdateOpenRequest>({
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);
});
});

View File

@@ -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]
/// <reference no-default-lib="true"/>
interface Boolean {}
interface Function {}
interface CallableFunction {}
interface NewableFunction {}
interface IArguments {}
interface Number { toExponential: any; }
interface Object {}
interface RegExp {}
interface String { charAt: any; }
interface Array<T> { 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 "/// <reference no-default-lib=\"true\"/>\ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array<T> { 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 "/// <reference no-default-lib=\"true\"/>\ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array<T> { 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