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