1namespace ts.projectSystem { 2 export import TI = server.typingsInstaller; 3 export import protocol = server.protocol; 4 export import CommandNames = server.CommandNames; 5 6 export import TestServerHost = TestFSWithWatch.TestServerHost; 7 export type File = TestFSWithWatch.File; 8 export type SymLink = TestFSWithWatch.SymLink; 9 export type Folder = TestFSWithWatch.Folder; 10 export import createServerHost = TestFSWithWatch.createServerHost; 11 export import checkArray = TestFSWithWatch.checkArray; 12 export import libFile = TestFSWithWatch.libFile; 13 14 export import commonFile1 = tscWatch.commonFile1; 15 export import commonFile2 = tscWatch.commonFile2; 16 17 const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; 18 export function mapOutputToJson(s: string) { 19 return convertToObject( 20 parseJsonText("json.json", s.replace(outputEventRegex, "")), 21 [] 22 ); 23 } 24 25 export const customTypesMap = { 26 path: "/typesMap.json" as Path, 27 content: `{ 28 "typesMap": { 29 "jquery": { 30 "match": "jquery(-(\\\\.?\\\\d+)+)?(\\\\.intellisense)?(\\\\.min)?\\\\.js$", 31 "types": ["jquery"] 32 }, 33 "quack": { 34 "match": "/duckquack-(\\\\d+)\\\\.min\\\\.js", 35 "types": ["duck-types"] 36 } 37 }, 38 "simpleMap": { 39 "Bacon": "baconjs", 40 "bliss": "blissfuljs", 41 "commander": "commander", 42 "cordova": "cordova", 43 "react": "react", 44 "lodash": "lodash" 45 } 46 }` 47 }; 48 49 export interface PostExecAction { 50 readonly success: boolean; 51 readonly callback: TI.RequestCompletedAction; 52 } 53 54 export interface Logger extends server.Logger { 55 logs: string[]; 56 host?: TestServerHost; 57 } 58 59 export function nullLogger(): Logger { 60 return { 61 close: noop, 62 hasLevel: returnFalse, 63 loggingEnabled: returnFalse, 64 perftrc: noop, 65 info: noop, 66 msg: noop, 67 startGroup: noop, 68 endGroup: noop, 69 getLogFileName: returnUndefined, 70 logs: [], 71 }; 72 } 73 74 export function createHasErrorMessageLogger(): Logger { 75 return { 76 ...nullLogger(), 77 msg: (s, type) => Debug.fail(`Error: ${s}, type: ${type}`), 78 }; 79 } 80 81 function handleLoggerGroup(logger: Logger, host: TestServerHost | undefined): Logger { 82 let inGroup = false; 83 let firstInGroup = false; 84 let seq = 0; 85 logger.startGroup = () => { 86 inGroup = true; 87 firstInGroup = true; 88 }; 89 logger.endGroup = () => inGroup = false; 90 logger.host = host; 91 const originalInfo = logger.info; 92 logger.info = s => msg(s, server.Msg.Info, s => originalInfo.call(logger, s)); 93 return logger; 94 95 function msg(s: string, type = server.Msg.Err, write: (s: string) => void) { 96 s = `[${nowString()}] ${s}`; 97 if (!inGroup || firstInGroup) s = padStringRight(type + " " + seq.toString(), " ") + s; 98 if (Debug.isDebugging) console.log(s); 99 write(s); 100 if (!inGroup) seq++; 101 } 102 103 function padStringRight(str: string, padding: string) { 104 return (str + padding).slice(0, padding.length); 105 } 106 107 function nowString() { 108 // E.g. "12:34:56.789" 109 const d = logger.host!.now(); 110 return `${padLeft(d.getUTCHours().toString(), 2, "0")}:${padLeft(d.getUTCMinutes().toString(), 2, "0")}:${padLeft(d.getUTCSeconds().toString(), 2, "0")}.${padLeft(d.getUTCMilliseconds().toString(), 3, "0")}`; 111 } 112 } 113 114 export function createLoggerWritingToConsole(host: TestServerHost): Logger { 115 return handleLoggerGroup({ 116 ...nullLogger(), 117 hasLevel: returnTrue, 118 loggingEnabled: returnTrue, 119 perftrc: s => console.log(s), 120 info: s => console.log(s), 121 msg: (s, type) => console.log(`${type}:: ${s}`), 122 }, host); 123 } 124 125 export function createLoggerWithInMemoryLogs(host: TestServerHost): Logger { 126 const logger = createHasErrorMessageLogger(); 127 return handleLoggerGroup({ 128 ...logger, 129 hasLevel: returnTrue, 130 loggingEnabled: returnTrue, 131 info: s => logger.logs.push( 132 s.replace(/Elapsed::?\s*\d+(?:\.\d+)?ms/g, "Elapsed:: *ms") 133 .replace(/\"updateGraphDurationMs\"\:\d+(?:\.\d+)?/g, `"updateGraphDurationMs":*`) 134 .replace(/\"createAutoImportProviderProgramDurationMs\"\:\d+(?:\.\d+)?/g, `"createAutoImportProviderProgramDurationMs":*`) 135 .replace(`"version":"${version}"`, `"version":"FakeVersion"`) 136 .replace(/getCompletionData: Get current token: \d+(?:\.\d+)?/g, `getCompletionData: Get current token: *`) 137 .replace(/getCompletionData: Is inside comment: \d+(?:\.\d+)?/g, `getCompletionData: Is inside comment: *`) 138 .replace(/getCompletionData: Get previous token: \d+(?:\.\d+)?/g, `getCompletionData: Get previous token: *`) 139 .replace(/getCompletionsAtPosition: isCompletionListBlocker: \d+(?:\.\d+)?/g, `getCompletionsAtPosition: isCompletionListBlocker: *`) 140 .replace(/getCompletionData: Semantic work: \d+(?:\.\d+)?/g, `getCompletionData: Semantic work: *`) 141 .replace(/getCompletionsAtPosition: getCompletionEntriesFromSymbols: \d+(?:\.\d+)?/g, `getCompletionsAtPosition: getCompletionEntriesFromSymbols: *`) 142 .replace(/forEachExternalModuleToImportFrom autoImportProvider: \d+(?:\.\d+)?/g, `forEachExternalModuleToImportFrom autoImportProvider: *`) 143 .replace(/getExportInfoMap: done in \d+(?:\.\d+)?/g, `getExportInfoMap: done in *`) 144 .replace(/collectAutoImports: \d+(?:\.\d+)?/g, `collectAutoImports: *`) 145 .replace(/dependencies in \d+(?:\.\d+)?/g, `dependencies in *`) 146 .replace(/\"exportMapKey\"\:\s*\"[_$a-zA-Z][_$_$a-zA-Z0-9]*\|\d+\|/g, match => match.replace(/\|\d+\|/, `|*|`)) 147 ) 148 }, host); 149 } 150 151 export function baselineTsserverLogs(scenario: string, subScenario: string, sessionOrService: { logger: Logger; }) { 152 Debug.assert(sessionOrService.logger.logs.length); // Ensure caller used in memory logger 153 Harness.Baseline.runBaseline(`tsserver/${scenario}/${subScenario.split(" ").join("-")}.js`, sessionOrService.logger.logs.join("\r\n")); 154 } 155 156 export function appendAllScriptInfos(service: server.ProjectService, logs: string[]) { 157 logs.push(""); 158 logs.push(`ScriptInfos:`); 159 service.filenameToScriptInfo.forEach(info => logs.push(`path: ${info.path} fileName: ${info.fileName}`)); 160 logs.push(""); 161 } 162 163 export function appendProjectFileText(project: server.Project, logs: string[]) { 164 logs.push(""); 165 logs.push(`Project: ${project.getProjectName()}`); 166 project.getCurrentProgram()?.getSourceFiles().forEach(f => { 167 logs.push(JSON.stringify({ fileName: f.fileName, version: f.version })); 168 logs.push(f.text); 169 logs.push(""); 170 }); 171 logs.push(""); 172 } 173 174 export class TestTypingsInstaller extends TI.TypingsInstaller implements server.ITypingsInstaller { 175 protected projectService!: server.ProjectService; 176 constructor( 177 readonly globalTypingsCacheLocation: string, 178 throttleLimit: number, 179 installTypingHost: server.ServerHost, 180 readonly typesRegistry = new Map<string, MapLike<string>>(), 181 log?: TI.Log) { 182 super(installTypingHost, globalTypingsCacheLocation, "/safeList.json" as Path, customTypesMap.path, throttleLimit, log); 183 } 184 185 protected postExecActions: PostExecAction[] = []; 186 187 isKnownTypesPackageName = notImplemented; 188 installPackage = notImplemented; 189 inspectValue = notImplemented; 190 191 executePendingCommands() { 192 const actionsToRun = this.postExecActions; 193 this.postExecActions = []; 194 for (const action of actionsToRun) { 195 action.callback(action.success); 196 } 197 } 198 199 checkPendingCommands(expectedCount: number) { 200 assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`); 201 } 202 203 onProjectClosed = noop; 204 205 attach(projectService: server.ProjectService) { 206 this.projectService = projectService; 207 } 208 209 getInstallTypingHost() { 210 return this.installTypingHost; 211 } 212 213 installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { 214 this.addPostExecAction("success", cb); 215 } 216 217 sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) { 218 this.projectService.updateTypingsForProject(response); 219 } 220 221 enqueueInstallTypingsRequest(project: server.Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>) { 222 const request = server.createInstallTypingsRequest(project, typeAcquisition, unresolvedImports, this.globalTypingsCacheLocation); 223 this.install(request); 224 } 225 226 addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) { 227 const out = isString(stdout) ? stdout : createNpmPackageJsonString(stdout); 228 const action: PostExecAction = { 229 success: !!out, 230 callback: cb 231 }; 232 this.postExecActions.push(action); 233 } 234 } 235 236 function createNpmPackageJsonString(installedTypings: string[]): string { 237 const dependencies: MapLike<any> = {}; 238 for (const typing of installedTypings) { 239 dependencies[typing] = "1.0.0"; 240 } 241 return JSON.stringify({ dependencies }); 242 } 243 244 export function createTypesRegistry(...list: string[]): ESMap<string, MapLike<string>> { 245 const versionMap = { 246 "latest": "1.3.0", 247 "ts2.0": "1.0.0", 248 "ts2.1": "1.0.0", 249 "ts2.2": "1.2.0", 250 "ts2.3": "1.3.0", 251 "ts2.4": "1.3.0", 252 "ts2.5": "1.3.0", 253 "ts2.6": "1.3.0", 254 "ts2.7": "1.3.0" 255 }; 256 const map = new Map<string, MapLike<string>>(); 257 for (const l of list) { 258 map.set(l, versionMap); 259 } 260 return map; 261 } 262 263 export function toExternalFile(fileName: string): protocol.ExternalFile { 264 return { fileName }; 265 } 266 267 export function toExternalFiles(fileNames: string[]) { 268 return map(fileNames, toExternalFile); 269 } 270 271 export function fileStats(nonZeroStats: Partial<server.FileStats>): server.FileStats { 272 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, ets: 0, etsSize: 0, dets: 0, detsSize: 0, ...nonZeroStats }; 273 } 274 275 export class TestServerEventManager { 276 private events: server.ProjectServiceEvent[] = []; 277 readonly session: TestSession; 278 readonly service: server.ProjectService; 279 readonly host: TestServerHost; 280 constructor(files: File[], suppressDiagnosticEvents?: boolean) { 281 this.host = createServerHost(files); 282 this.session = createSession(this.host, { 283 canUseEvents: true, 284 eventHandler: event => this.events.push(event), 285 suppressDiagnosticEvents, 286 }); 287 this.service = this.session.getProjectService(); 288 } 289 290 getEvents(): readonly server.ProjectServiceEvent[] { 291 const events = this.events; 292 this.events = []; 293 return events; 294 } 295 296 getEvent<T extends server.ProjectServiceEvent>(eventName: T["eventName"]): T["data"] { 297 let eventData: T["data"] | undefined; 298 filterMutate(this.events, e => { 299 if (e.eventName === eventName) { 300 if (eventData !== undefined) { 301 assert(false, "more than one event found"); 302 } 303 eventData = e.data; 304 return false; 305 } 306 return true; 307 }); 308 return Debug.checkDefined(eventData); 309 } 310 311 hasZeroEvent<T extends server.ProjectServiceEvent>(eventName: T["eventName"]) { 312 this.events.forEach(event => assert.notEqual(event.eventName, eventName)); 313 } 314 315 assertProjectInfoTelemetryEvent(partial: Partial<server.ProjectInfoTelemetryEventData>, configFile = "/tsconfig.json"): void { 316 assert.deepEqual<server.ProjectInfoTelemetryEventData>(this.getEvent<server.ProjectInfoTelemetryEvent>(server.ProjectInfoTelemetryEvent), { 317 projectId: sys.createSHA256Hash!(configFile), 318 fileStats: fileStats({ ts: 1 }), 319 compilerOptions: {}, 320 extends: false, 321 files: false, 322 include: false, 323 exclude: false, 324 compileOnSave: false, 325 typeAcquisition: { 326 enable: false, 327 exclude: false, 328 include: false, 329 }, 330 configFileName: "tsconfig.json", 331 projectType: "configured", 332 languageServiceEnabled: true, 333 version: ts.version, // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier 334 ...partial, 335 }); 336 } 337 338 assertOpenFileTelemetryEvent(info: server.OpenFileInfo): void { 339 assert.deepEqual<server.OpenFileInfoTelemetryEventData>(this.getEvent<server.OpenFileInfoTelemetryEvent>(server.OpenFileInfoTelemetryEvent), { info }); 340 } 341 assertNoOpenFilesTelemetryEvent(): void { 342 this.hasZeroEvent<server.OpenFileInfoTelemetryEvent>(server.OpenFileInfoTelemetryEvent); 343 } 344 } 345 346 export type TestSessionAndServiceHost = TestFSWithWatch.TestServerHostTrackingWrittenFiles & { 347 baselineHost(title: string): void; 348 }; 349 function patchHostTimeouts( 350 inputHost: TestFSWithWatch.TestServerHostTrackingWrittenFiles, 351 session: TestSession | TestProjectService 352 ) { 353 const host = inputHost as TestSessionAndServiceHost; 354 const originalCheckTimeoutQueueLength = host.checkTimeoutQueueLength; 355 const originalRunQueuedTimeoutCallbacks = host.runQueuedTimeoutCallbacks; 356 const originalRunQueuedImmediateCallbacks = host.runQueuedImmediateCallbacks; 357 let hostDiff: ReturnType<TestServerHost["snap"]> | undefined; 358 359 host.checkTimeoutQueueLengthAndRun = checkTimeoutQueueLengthAndRun; 360 host.checkTimeoutQueueLength = checkTimeoutQueueLength; 361 host.runQueuedTimeoutCallbacks = runQueuedTimeoutCallbacks; 362 host.runQueuedImmediateCallbacks = runQueuedImmediateCallbacks; 363 host.baselineHost = baselineHost; 364 return host; 365 366 function checkTimeoutQueueLengthAndRun(expected: number) { 367 host.baselineHost(`Before checking timeout queue length (${expected}) and running`); 368 originalCheckTimeoutQueueLength.call(host, expected); 369 originalRunQueuedTimeoutCallbacks.call(host); 370 host.baselineHost(`After checking timeout queue length (${expected}) and running`); 371 } 372 373 function checkTimeoutQueueLength(expected: number) { 374 host.baselineHost(`Checking timeout queue length: ${expected}`); 375 originalCheckTimeoutQueueLength.call(host, expected); 376 } 377 378 function runQueuedTimeoutCallbacks(timeoutId?: number) { 379 host.baselineHost(`Before running timeout callback${timeoutId === undefined ? "s" : timeoutId}`); 380 originalRunQueuedTimeoutCallbacks.call(host, timeoutId); 381 host.baselineHost(`After running timeout callback${timeoutId === undefined ? "s" : timeoutId}`); 382 } 383 384 function runQueuedImmediateCallbacks(checkCount?: number) { 385 host.baselineHost(`Before running immediate callbacks${checkCount === undefined ? "" : ` and checking length (${checkCount})`}`); 386 originalRunQueuedImmediateCallbacks.call(host, checkCount); 387 host.baselineHost(`Before running immediate callbacks${checkCount === undefined ? "" : ` and checking length (${checkCount})`}`); 388 } 389 390 function baselineHost(title: string) { 391 if (!session.logger.hasLevel(server.LogLevel.verbose)) return; 392 session.logger.logs.push(title); 393 host.diff(session.logger.logs, hostDiff); 394 host.serializeWatches(session.logger.logs); 395 hostDiff = host.snap(); 396 host.writtenFiles.clear(); 397 } 398 } 399 400 export interface TestSessionOptions extends server.SessionOptions { 401 logger: Logger; 402 } 403 404 export class TestSession extends server.Session { 405 private seq = 0; 406 public events: protocol.Event[] = []; 407 public testhost: TestSessionAndServiceHost; 408 public logger: Logger; 409 410 constructor(opts: TestSessionOptions) { 411 super(opts); 412 this.logger = opts.logger; 413 this.testhost = patchHostTimeouts( 414 TestFSWithWatch.changeToHostTrackingWrittenFiles(this.host as TestServerHost), 415 this 416 ); 417 } 418 419 getProjectService() { 420 return this.projectService; 421 } 422 423 public getSeq() { 424 return this.seq; 425 } 426 427 public getNextSeq() { 428 return this.seq + 1; 429 } 430 431 public executeCommand(request: protocol.Request) { 432 return this.baseline("response", super.executeCommand(this.baseline("request", request))); 433 } 434 435 public executeCommandSeq<T extends server.protocol.Request>(request: Partial<T>) { 436 this.seq++; 437 request.seq = this.seq; 438 request.type = "request"; 439 return this.executeCommand(request as T); 440 } 441 442 public event<T extends object>(body: T, eventName: string) { 443 this.events.push(server.toEvent(eventName, body)); 444 super.event(body, eventName); 445 } 446 447 public clearMessages() { 448 clear(this.events); 449 this.testhost.clearOutput(); 450 } 451 452 private baseline<T extends protocol.Request | server.HandlerResponse>(type: "request" | "response", requestOrResult: T): T { 453 if (!this.logger.hasLevel(server.LogLevel.verbose)) return requestOrResult; 454 if (type === "request") this.logger.info(`request:${server.indent(JSON.stringify(requestOrResult, undefined, 2))}`); 455 this.testhost.baselineHost(type === "request" ? "Before request" : "After request"); 456 if (type === "response") this.logger.info(`response:${server.indent(JSON.stringify(requestOrResult, undefined, 2))}`); 457 return requestOrResult; 458 } 459 } 460 461 export function createSession(host: server.ServerHost, opts: Partial<TestSessionOptions> = {}) { 462 if (opts.typingsInstaller === undefined) { 463 opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); 464 } 465 466 if (opts.eventHandler !== undefined) { 467 opts.canUseEvents = true; 468 } 469 470 const sessionOptions: TestSessionOptions = { 471 host, 472 cancellationToken: server.nullCancellationToken, 473 useSingleInferredProject: false, 474 useInferredProjectPerProjectRoot: false, 475 typingsInstaller: undefined!, // TODO: GH#18217 476 byteLength: Utils.byteLength, 477 hrtime: process.hrtime, 478 logger: opts.logger || createHasErrorMessageLogger(), 479 canUseEvents: false 480 }; 481 482 return new TestSession({ ...sessionOptions, ...opts }); 483 } 484 485 export function createSessionWithEventTracking<T extends server.ProjectServiceEvent>(host: server.ServerHost, eventNames: T["eventName"] | T["eventName"][], opts: Partial<TestSessionOptions> = {}) { 486 const events: T[] = []; 487 const session = createSession(host, { 488 eventHandler: e => { 489 if (isArray(eventNames) ? eventNames.some(eventName => e.eventName === eventName) : eventNames === e.eventName) { 490 events.push(e as T); 491 } 492 }, 493 ...opts 494 }); 495 496 return { session, events }; 497 } 498 499 export function createSessionWithDefaultEventHandler<T extends protocol.AnyEvent>(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial<TestSessionOptions> = {}) { 500 const session = createSession(host, { canUseEvents: true, ...opts }); 501 502 return { 503 session, 504 getEvents, 505 clearEvents 506 }; 507 508 function getEvents() { 509 return mapDefined(host.getOutput(), s => { 510 const e = mapOutputToJson(s); 511 return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined; 512 }); 513 } 514 515 function clearEvents() { 516 session.clearMessages(); 517 } 518 } 519 520 export interface TestProjectServiceOptions extends server.ProjectServiceOptions { 521 logger: Logger; 522 } 523 524 export class TestProjectService extends server.ProjectService { 525 public testhost: TestSessionAndServiceHost; 526 constructor(host: TestServerHost, public logger: Logger, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, 527 typingsInstaller: server.ITypingsInstaller, opts: Partial<TestProjectServiceOptions> = {}) { 528 super({ 529 host, 530 logger, 531 session: undefined, 532 cancellationToken, 533 useSingleInferredProject, 534 useInferredProjectPerProjectRoot: false, 535 typingsInstaller, 536 typesMapLocation: customTypesMap.path, 537 ...opts 538 }); 539 this.testhost = patchHostTimeouts( 540 TestFSWithWatch.changeToHostTrackingWrittenFiles(this.host as TestServerHost), 541 this 542 ); 543 this.testhost.baselineHost("Creating project service"); 544 } 545 546 checkNumberOfProjects(count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { 547 checkNumberOfProjects(this, count); 548 } 549 } 550 551 export function createProjectService(host: TestServerHost, options?: Partial<TestProjectServiceOptions>) { 552 const cancellationToken = options?.cancellationToken || server.nullCancellationToken; 553 const logger = options?.logger || createHasErrorMessageLogger(); 554 const useSingleInferredProject = options?.useSingleInferredProject !== undefined ? options.useSingleInferredProject : false; 555 return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, options?.typingsInstaller || server.nullTypingsInstaller, options); 556 } 557 558 export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { 559 assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); 560 } 561 562 export function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) { 563 assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`); 564 } 565 566 export function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) { 567 assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); 568 } 569 570 export function checkNumberOfProjects(projectService: server.ProjectService, count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { 571 checkNumberOfConfiguredProjects(projectService, count.configuredProjects || 0); 572 checkNumberOfExternalProjects(projectService, count.externalProjects || 0); 573 checkNumberOfInferredProjects(projectService, count.inferredProjects || 0); 574 } 575 576 export function configuredProjectAt(projectService: server.ProjectService, index: number) { 577 const values = projectService.configuredProjects.values(); 578 while (index > 0) { 579 const iterResult = values.next(); 580 if (iterResult.done) return Debug.fail("Expected a result."); 581 index--; 582 } 583 const iterResult = values.next(); 584 if (iterResult.done) return Debug.fail("Expected a result."); 585 return iterResult.value; 586 } 587 588 export function checkProjectActualFiles(project: server.Project, expectedFiles: readonly string[]) { 589 checkArray(`${server.ProjectKind[project.projectKind]} project: ${project.getProjectName()}:: actual files`, project.getFileNames(), expectedFiles); 590 } 591 592 export function checkProjectRootFiles(project: server.Project, expectedFiles: readonly string[]) { 593 checkArray(`${server.ProjectKind[project.projectKind]} project: ${project.getProjectName()}::, rootFileNames`, project.getRootFiles(), expectedFiles); 594 } 595 596 export function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { 597 dir = normalizePath(dir); 598 const result: string[] = []; 599 forEachAncestorDirectory(dir, ancestor => { 600 if (mapAncestor(ancestor)) { 601 result.push(combinePaths(ancestor, path2)); 602 } 603 }); 604 return result; 605 } 606 607 export function getRootsToWatchWithAncestorDirectory(dir: string, path2: string) { 608 return mapCombinedPathsInAncestor(dir, path2, ancestor => ancestor.split(directorySeparator).length > 4); 609 } 610 611 export const nodeModules = "node_modules"; 612 export function getNodeModuleDirectories(dir: string) { 613 return getRootsToWatchWithAncestorDirectory(dir, nodeModules); 614 } 615 616 export const nodeModulesAtTypes = "node_modules/@types"; 617 export function getTypeRootsFromLocation(currentDirectory: string) { 618 return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes); 619 } 620 621 export function getConfigFilesToWatch(folder: string) { 622 return [ 623 ...getRootsToWatchWithAncestorDirectory(folder, "tsconfig.json"), 624 ...getRootsToWatchWithAncestorDirectory(folder, "jsconfig.json") 625 ]; 626 } 627 628 export function protocolLocationFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.Location { 629 const start = nthIndexOf(str, substring, options ? options.index : 0); 630 Debug.assert(start !== -1); 631 return protocolToLocation(str)(start); 632 } 633 634 export function protocolToLocation(text: string): (pos: number) => protocol.Location { 635 const lineStarts = computeLineStarts(text); 636 return pos => { 637 const x = computeLineAndCharacterOfPosition(lineStarts, pos); 638 return { line: x.line + 1, offset: x.character + 1 }; 639 }; 640 } 641 642 export function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan { 643 const span = textSpanFromSubstring(str, substring, options); 644 const toLocation = protocolToLocation(str); 645 return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) }; 646 } 647 648 export interface DocumentSpanFromSubstring { 649 file: File; 650 text: string; 651 options?: SpanFromSubstringOptions; 652 contextText?: string; 653 contextOptions?: SpanFromSubstringOptions; 654 } 655 export function protocolFileSpanFromSubstring({ file, text, options }: DocumentSpanFromSubstring): protocol.FileSpan { 656 return { file: file.path, ...protocolTextSpanFromSubstring(file.content, text, options) }; 657 } 658 659 interface FileSpanWithContextFromSubString { 660 file: File; 661 text: string; 662 options?: SpanFromSubstringOptions; 663 contextText?: string; 664 contextOptions?: SpanFromSubstringOptions; 665 } 666 export function protocolFileSpanWithContextFromSubstring({ contextText, contextOptions, ...rest }: FileSpanWithContextFromSubString): protocol.FileSpanWithContext { 667 const result = protocolFileSpanFromSubstring(rest); 668 const contextSpan = contextText !== undefined ? 669 protocolFileSpanFromSubstring({ file: rest.file, text: contextText, options: contextOptions }) : 670 undefined; 671 return contextSpan ? 672 { 673 ...result, 674 contextStart: contextSpan.start, 675 contextEnd: contextSpan.end 676 } : 677 result; 678 } 679 680 export interface ProtocolTextSpanWithContextFromString { 681 fileText: string; 682 text: string; 683 options?: SpanFromSubstringOptions; 684 contextText?: string; 685 contextOptions?: SpanFromSubstringOptions; 686 } 687 export function protocolTextSpanWithContextFromSubstring({ fileText, text, options, contextText, contextOptions }: ProtocolTextSpanWithContextFromString): protocol.TextSpanWithContext { 688 const span = textSpanFromSubstring(fileText, text, options); 689 const toLocation = protocolToLocation(fileText); 690 const contextSpan = contextText !== undefined ? textSpanFromSubstring(fileText, contextText, contextOptions) : undefined; 691 return { 692 start: toLocation(span.start), 693 end: toLocation(textSpanEnd(span)), 694 ...contextSpan && { 695 contextStart: toLocation(contextSpan.start), 696 contextEnd: toLocation(textSpanEnd(contextSpan)) 697 } 698 }; 699 } 700 701 export interface ProtocolRenameSpanFromSubstring extends ProtocolTextSpanWithContextFromString { 702 prefixSuffixText?: { 703 readonly prefixText?: string; 704 readonly suffixText?: string; 705 }; 706 } 707 export function protocolRenameSpanFromSubstring({ prefixSuffixText, ...rest }: ProtocolRenameSpanFromSubstring): protocol.RenameTextSpan { 708 return { 709 ...protocolTextSpanWithContextFromSubstring(rest), 710 ...prefixSuffixText 711 }; 712 } 713 714 export function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan { 715 const start = nthIndexOf(str, substring, options ? options.index : 0); 716 Debug.assert(start !== -1); 717 return createTextSpan(start, substring.length); 718 } 719 720 export function protocolFileLocationFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileLocationRequestArgs { 721 return { file: file.path, ...protocolLocationFromSubstring(file.content, substring, options) }; 722 } 723 724 export interface SpanFromSubstringOptions { 725 readonly index: number; 726 } 727 728 function nthIndexOf(str: string, substr: string, n: number): number { 729 let index = -1; 730 for (; n >= 0; n--) { 731 index = str.indexOf(substr, index + 1); 732 if (index === -1) return -1; 733 } 734 return index; 735 } 736 737 /** 738 * Test server cancellation token used to mock host token cancellation requests. 739 * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls 740 * should be made before canceling the token. The id of the request to cancel should be set with 741 * setRequestToCancel(); 742 */ 743 export class TestServerCancellationToken implements server.ServerCancellationToken { 744 private currentId: number | undefined = -1; 745 private requestToCancel = -1; 746 private isCancellationRequestedCount = 0; 747 748 constructor(private cancelAfterRequest = 0) { 749 } 750 751 setRequest(requestId: number) { 752 this.currentId = requestId; 753 } 754 755 setRequestToCancel(requestId: number) { 756 this.resetToken(); 757 this.requestToCancel = requestId; 758 } 759 760 resetRequest(requestId: number) { 761 assert.equal(requestId, this.currentId, "unexpected request id in cancellation"); 762 this.currentId = undefined; 763 } 764 765 isCancellationRequested() { 766 this.isCancellationRequestedCount++; 767 // If the request id is the request to cancel and isCancellationRequestedCount 768 // has been met then cancel the request. Ex: cancel the request if it is a 769 // nav bar request & isCancellationRequested() has already been called three times. 770 return this.requestToCancel === this.currentId && this.isCancellationRequestedCount >= this.cancelAfterRequest; 771 } 772 773 resetToken() { 774 this.currentId = -1; 775 this.isCancellationRequestedCount = 0; 776 this.requestToCancel = -1; 777 } 778 } 779 780 export function makeSessionRequest<T>(command: string, args: T): protocol.Request { 781 return { 782 seq: 0, 783 type: "request", 784 command, 785 arguments: args 786 }; 787 } 788 789 export function executeSessionRequest<TRequest extends protocol.Request, TResponse extends protocol.Response>(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] { 790 return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"]; 791 } 792 793 export function executeSessionRequestNoResponse<TRequest extends protocol.Request>(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): void { 794 session.executeCommand(makeSessionRequest(command, args)); 795 } 796 797 export function openFilesForSession(files: readonly (File | { readonly file: File | string, readonly projectRootPath: string, content?: string })[], session: server.Session): void { 798 for (const file of files) { 799 session.executeCommand(makeSessionRequest<protocol.OpenRequestArgs>(CommandNames.Open, 800 "projectRootPath" in file ? { file: typeof file.file === "string" ? file.file : file.file.path, projectRootPath: file.projectRootPath } : { file: file.path })); // eslint-disable-line local/no-in-operator 801 } 802 } 803 804 export function closeFilesForSession(files: readonly File[], session: server.Session): void { 805 for (const file of files) { 806 session.executeCommand(makeSessionRequest<protocol.FileRequestArgs>(CommandNames.Close, { file: file.path })); 807 } 808 } 809 810 export interface MakeReferenceItem extends DocumentSpanFromSubstring { 811 isDefinition?: boolean; 812 isWriteAccess?: boolean; 813 lineText?: string; 814 } 815 816 export function makeReferenceItem({ isDefinition, isWriteAccess, lineText, ...rest }: MakeReferenceItem): protocol.ReferencesResponseItem { 817 return { 818 ...protocolFileSpanWithContextFromSubstring(rest), 819 isDefinition, 820 isWriteAccess: isWriteAccess === undefined ? !!isDefinition : isWriteAccess, 821 lineText, 822 }; 823 } 824 825 export interface VerifyGetErrRequestBase { 826 session: TestSession; 827 host: TestServerHost; 828 existingTimeouts?: number; 829 } 830 export interface VerifyGetErrRequest extends VerifyGetErrRequestBase { 831 files: readonly (string | File)[]; 832 skip?: CheckAllErrors["skip"]; 833 } 834 export function verifyGetErrRequest(request: VerifyGetErrRequest) { 835 const { session, files } = request; 836 session.executeCommandSeq<protocol.GeterrRequest>({ 837 command: protocol.CommandTypes.Geterr, 838 arguments: { delay: 0, files: files.map(filePath) } 839 }); 840 checkAllErrors(request); 841 } 842 843 interface SkipErrors { semantic?: true; suggestion?: true } 844 export interface CheckAllErrors extends VerifyGetErrRequestBase { 845 files: readonly any[]; 846 skip?: readonly (SkipErrors | undefined)[]; 847 } 848 function checkAllErrors({ session, host, existingTimeouts, files, skip }: CheckAllErrors) { 849 Debug.assert(session.logger.logs.length); 850 for (let i = 0; i < files.length; i++) { 851 if (existingTimeouts !== undefined) { 852 host.checkTimeoutQueueLength(existingTimeouts + 1); 853 host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1); 854 } 855 else { 856 host.checkTimeoutQueueLengthAndRun(1); 857 } 858 if (!skip?.[i]?.semantic) host.runQueuedImmediateCallbacks(1); 859 if (!skip?.[i]?.suggestion) host.runQueuedImmediateCallbacks(1); 860 } 861 } 862 863 function filePath(file: string | File) { 864 return isString(file) ? file : file.path; 865 } 866 867 function verifyErrorsUsingGeterr({scenario, subScenario, allFiles, openFiles, getErrRequest }: VerifyGetErrScenario) { 868 it("verifies the errors in open file", () => { 869 const host = createServerHost([...allFiles(), libFile]); 870 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 871 openFilesForSession(openFiles(), session); 872 873 verifyGetErrRequest({ session, host, files: getErrRequest() }); 874 baselineTsserverLogs(scenario, `${subScenario} getErr`, session); 875 }); 876 } 877 878 function verifyErrorsUsingGeterrForProject({ scenario, subScenario, allFiles, openFiles, getErrForProjectRequest }: VerifyGetErrScenario) { 879 it("verifies the errors in projects", () => { 880 const host = createServerHost([...allFiles(), libFile]); 881 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 882 openFilesForSession(openFiles(), session); 883 884 for (const expected of getErrForProjectRequest()) { 885 session.executeCommandSeq<protocol.GeterrForProjectRequest>({ 886 command: protocol.CommandTypes.GeterrForProject, 887 arguments: { delay: 0, file: filePath(expected.project) } 888 }); 889 checkAllErrors({ session, host, files: expected.files }); 890 } 891 baselineTsserverLogs(scenario, `${subScenario} geterrForProject`, session); 892 }); 893 } 894 895 function verifyErrorsUsingSyncMethods({ scenario, subScenario, allFiles, openFiles, syncDiagnostics }: VerifyGetErrScenario) { 896 it("verifies the errors using sync commands", () => { 897 const host = createServerHost([...allFiles(), libFile]); 898 const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); 899 openFilesForSession(openFiles(), session); 900 for (const { file, project } of syncDiagnostics()) { 901 const reqArgs = { file: filePath(file), projectFileName: project && filePath(project) }; 902 session.executeCommandSeq<protocol.SyntacticDiagnosticsSyncRequest>({ 903 command: protocol.CommandTypes.SyntacticDiagnosticsSync, 904 arguments: reqArgs 905 }); 906 session.executeCommandSeq<protocol.SemanticDiagnosticsSyncRequest>({ 907 command: protocol.CommandTypes.SemanticDiagnosticsSync, 908 arguments: reqArgs 909 }); 910 session.executeCommandSeq<protocol.SuggestionDiagnosticsSyncRequest>({ 911 command: protocol.CommandTypes.SuggestionDiagnosticsSync, 912 arguments: reqArgs 913 }); 914 } 915 baselineTsserverLogs(scenario, `${subScenario} gerErr with sync commands`, session); 916 }); 917 } 918 919 export interface GetErrForProjectDiagnostics { 920 project: string | File; 921 files: readonly (string | File)[]; 922 skip?: CheckAllErrors["skip"]; 923 } 924 export interface SyncDiagnostics { 925 file: string | File; 926 project?: string | File; 927 } 928 export interface VerifyGetErrScenario { 929 scenario: string; 930 subScenario: string; 931 allFiles: () => readonly File[]; 932 openFiles: () => readonly File[]; 933 getErrRequest: () => VerifyGetErrRequest["files"]; 934 getErrForProjectRequest: () => readonly GetErrForProjectDiagnostics[]; 935 syncDiagnostics: () => readonly SyncDiagnostics[]; 936 } 937 export function verifyGetErrScenario(scenario: VerifyGetErrScenario) { 938 verifyErrorsUsingGeterr(scenario); 939 verifyErrorsUsingGeterrForProject(scenario); 940 verifyErrorsUsingSyncMethods(scenario); 941 } 942} 943