1/** 2 * Fake implementations of various compiler dependencies. 3 */ 4namespace fakes { 5 const processExitSentinel = new Error("System exit"); 6 7 export interface SystemOptions { 8 executingFilePath?: string; 9 newLine?: "\r\n" | "\n"; 10 env?: Record<string, string>; 11 } 12 13 /** 14 * A fake `ts.System` that leverages a virtual file system. 15 */ 16 export class System implements ts.System { 17 public readonly vfs: vfs.FileSystem; 18 public readonly args: string[] = []; 19 public readonly output: string[] = []; 20 public readonly newLine: string; 21 public readonly useCaseSensitiveFileNames: boolean; 22 public exitCode: number | undefined; 23 24 private readonly _executingFilePath: string | undefined; 25 private readonly _env: Record<string, string> | undefined; 26 27 constructor(vfs: vfs.FileSystem, { executingFilePath, newLine = "\r\n", env }: SystemOptions = {}) { 28 this.vfs = vfs.isReadonly ? vfs.shadow() : vfs; 29 this.useCaseSensitiveFileNames = !this.vfs.ignoreCase; 30 this.newLine = newLine; 31 this._executingFilePath = executingFilePath; 32 this._env = env; 33 } 34 35 private testTerminalWidth = Number.parseInt(this.getEnvironmentVariable("TS_TEST_TERMINAL_WIDTH")); 36 getWidthOfTerminal = Number.isNaN(this.testTerminalWidth) ? undefined : () => this.testTerminalWidth; 37 38 // Pretty output 39 writeOutputIsTTY() { 40 return true; 41 } 42 43 public write(message: string) { 44 if (ts.Debug.isDebugging) console.log(message); 45 this.output.push(message); 46 } 47 48 public readFile(path: string) { 49 try { 50 const content = this.vfs.readFileSync(path, "utf8"); 51 return content === undefined ? undefined : Utils.removeByteOrderMark(content); 52 } 53 catch { 54 return undefined; 55 } 56 } 57 58 public writeFile(path: string, data: string, writeByteOrderMark?: boolean): void { 59 this.vfs.mkdirpSync(vpath.dirname(path)); 60 this.vfs.writeFileSync(path, writeByteOrderMark ? Utils.addUTF8ByteOrderMark(data) : data); 61 } 62 63 public deleteFile(path: string) { 64 this.vfs.unlinkSync(path); 65 } 66 67 public fileExists(path: string) { 68 const stats = this._getStats(path); 69 return stats ? stats.isFile() : false; 70 } 71 72 public directoryExists(path: string) { 73 const stats = this._getStats(path); 74 return stats ? stats.isDirectory() : false; 75 } 76 77 public createDirectory(path: string): void { 78 this.vfs.mkdirpSync(path); 79 } 80 81 public getCurrentDirectory() { 82 return this.vfs.cwd(); 83 } 84 85 public getDirectories(path: string) { 86 const result: string[] = []; 87 try { 88 for (const file of this.vfs.readdirSync(path)) { 89 if (this.vfs.statSync(vpath.combine(path, file)).isDirectory()) { 90 result.push(file); 91 } 92 } 93 } 94 catch { /*ignore*/ } 95 return result; 96 } 97 98 public readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { 99 return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, path => this.getAccessibleFileSystemEntries(path), path => this.realpath(path)); 100 } 101 102 public getAccessibleFileSystemEntries(path: string): ts.FileSystemEntries { 103 const files: string[] = []; 104 const directories: string[] = []; 105 try { 106 for (const file of this.vfs.readdirSync(path)) { 107 try { 108 const stats = this.vfs.statSync(vpath.combine(path, file)); 109 if (stats.isFile()) { 110 files.push(file); 111 } 112 else if (stats.isDirectory()) { 113 directories.push(file); 114 } 115 } 116 catch { /*ignored*/ } 117 } 118 } 119 catch { /*ignored*/ } 120 return { files, directories }; 121 } 122 123 public exit(exitCode?: number) { 124 this.exitCode = exitCode; 125 throw processExitSentinel; 126 } 127 128 public getFileSize(path: string) { 129 const stats = this._getStats(path); 130 return stats && stats.isFile() ? stats.size : 0; 131 } 132 133 public resolvePath(path: string) { 134 return vpath.resolve(this.vfs.cwd(), path); 135 } 136 137 public getExecutingFilePath() { 138 if (this._executingFilePath === undefined) return ts.notImplemented(); 139 return this._executingFilePath; 140 } 141 142 public getModifiedTime(path: string) { 143 const stats = this._getStats(path); 144 return stats ? stats.mtime : undefined!; // TODO: GH#18217 145 } 146 147 public setModifiedTime(path: string, time: Date) { 148 this.vfs.utimesSync(path, time, time); 149 } 150 151 public createHash(data: string): string { 152 return `${ts.generateDjb2Hash(data)}-${data}`; 153 } 154 155 public realpath(path: string) { 156 try { 157 return this.vfs.realpathSync(path); 158 } 159 catch { 160 return path; 161 } 162 } 163 164 public getEnvironmentVariable(name: string): string { 165 return (this._env && this._env[name])!; // TODO: GH#18217 166 } 167 168 private _getStats(path: string) { 169 try { 170 return this.vfs.existsSync(path) ? this.vfs.statSync(path) : undefined; 171 } 172 catch { 173 return undefined; 174 } 175 } 176 177 now() { 178 return new Date(this.vfs.time()); 179 } 180 } 181 182 /** 183 * A fake `ts.ParseConfigHost` that leverages a virtual file system. 184 */ 185 export class ParseConfigHost implements ts.ParseConfigHost { 186 public readonly sys: System; 187 188 constructor(sys: System | vfs.FileSystem) { 189 if (sys instanceof vfs.FileSystem) sys = new System(sys); 190 this.sys = sys; 191 } 192 193 public get vfs() { 194 return this.sys.vfs; 195 } 196 197 public get useCaseSensitiveFileNames() { 198 return this.sys.useCaseSensitiveFileNames; 199 } 200 201 public fileExists(fileName: string): boolean { 202 return this.sys.fileExists(fileName); 203 } 204 205 public directoryExists(directoryName: string): boolean { 206 return this.sys.directoryExists(directoryName); 207 } 208 209 public readFile(path: string): string | undefined { 210 return this.sys.readFile(path); 211 } 212 213 public readDirectory(path: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] { 214 return this.sys.readDirectory(path, extensions, excludes, includes, depth); 215 } 216 } 217 218 /** 219 * A fake `ts.CompilerHost` that leverages a virtual file system. 220 */ 221 export class CompilerHost implements ts.CompilerHost { 222 public readonly sys: System; 223 public readonly defaultLibLocation: string; 224 public readonly outputs: documents.TextDocument[] = []; 225 private readonly _outputsMap: collections.SortedMap<string, number>; 226 public readonly traces: string[] = []; 227 public readonly shouldAssertInvariants = !Harness.lightMode; 228 229 private _setParentNodes: boolean; 230 private _sourceFiles: collections.SortedMap<string, ts.SourceFile>; 231 private _parseConfigHost: ParseConfigHost | undefined; 232 private _newLine: string; 233 234 constructor(sys: System | vfs.FileSystem, options = ts.getDefaultCompilerOptions(), setParentNodes = false) { 235 if (sys instanceof vfs.FileSystem) sys = new System(sys); 236 this.sys = sys; 237 this.defaultLibLocation = sys.vfs.meta.get("defaultLibLocation") || ""; 238 this._newLine = ts.getNewLineCharacter(options, () => this.sys.newLine); 239 this._sourceFiles = new collections.SortedMap<string, ts.SourceFile>({ comparer: sys.vfs.stringComparer, sort: "insertion" }); 240 this._setParentNodes = setParentNodes; 241 this._outputsMap = new collections.SortedMap(this.vfs.stringComparer); 242 } 243 244 public get vfs() { 245 return this.sys.vfs; 246 } 247 248 public get parseConfigHost() { 249 return this._parseConfigHost || (this._parseConfigHost = new ParseConfigHost(this.sys)); 250 } 251 252 public getCurrentDirectory(): string { 253 return this.sys.getCurrentDirectory(); 254 } 255 256 public useCaseSensitiveFileNames(): boolean { 257 return this.sys.useCaseSensitiveFileNames; 258 } 259 260 public getNewLine(): string { 261 return this._newLine; 262 } 263 264 public getCanonicalFileName(fileName: string): string { 265 return this.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); 266 } 267 268 public deleteFile(fileName: string) { 269 this.sys.deleteFile(fileName); 270 } 271 272 public fileExists(fileName: string): boolean { 273 return this.sys.fileExists(fileName); 274 } 275 276 public directoryExists(directoryName: string): boolean { 277 return this.sys.directoryExists(directoryName); 278 } 279 280 public getModifiedTime(fileName: string) { 281 return this.sys.getModifiedTime(fileName); 282 } 283 284 public setModifiedTime(fileName: string, time: Date) { 285 return this.sys.setModifiedTime(fileName, time); 286 } 287 288 public getDirectories(path: string): string[] { 289 return this.sys.getDirectories(path); 290 } 291 292 public readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { 293 return this.sys.readDirectory(path, extensions, exclude, include, depth); 294 } 295 296 public readFile(path: string): string | undefined { 297 return this.sys.readFile(path); 298 } 299 300 public writeFile(fileName: string, content: string, writeByteOrderMark: boolean) { 301 if (writeByteOrderMark) content = Utils.addUTF8ByteOrderMark(content); 302 this.sys.writeFile(fileName, content); 303 304 const document = new documents.TextDocument(fileName, content); 305 document.meta.set("fileName", fileName); 306 this.vfs.filemeta(fileName).set("document", document); 307 if (!this._outputsMap.has(document.file)) { 308 this._outputsMap.set(document.file, this.outputs.length); 309 this.outputs.push(document); 310 } 311 this.outputs[this._outputsMap.get(document.file)!] = document; 312 } 313 314 public trace(s: string): void { 315 this.traces.push(s); 316 } 317 318 public realpath(path: string): string { 319 return this.sys.realpath(path); 320 } 321 322 public getDefaultLibLocation(): string { 323 return vpath.resolve(this.getCurrentDirectory(), this.defaultLibLocation); 324 } 325 326 public getDefaultLibFileName(options: ts.CompilerOptions): string { 327 return vpath.resolve(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options)); 328 } 329 330 public getSourceFile(fileName: string, languageVersion: number): ts.SourceFile | undefined { 331 const canonicalFileName = this.getCanonicalFileName(vpath.resolve(this.getCurrentDirectory(), fileName)); 332 const existing = this._sourceFiles.get(canonicalFileName); 333 if (existing) return existing; 334 335 const content = this.readFile(canonicalFileName); 336 if (content === undefined) return undefined; 337 338 // A virtual file system may shadow another existing virtual file system. This 339 // allows us to reuse a common virtual file system structure across multiple 340 // tests. If a virtual file is a shadow, it is likely that the file will be 341 // reused across multiple tests. In that case, we cache the SourceFile we parse 342 // so that it can be reused across multiple tests to avoid the cost of 343 // repeatedly parsing the same file over and over (such as lib.d.ts). 344 const cacheKey = this.vfs.shadowRoot && `SourceFile[languageVersion=${languageVersion},setParentNodes=${this._setParentNodes}]`; 345 if (cacheKey) { 346 const meta = this.vfs.filemeta(canonicalFileName); 347 const sourceFileFromMetadata = meta.get(cacheKey) as ts.SourceFile | undefined; 348 if (sourceFileFromMetadata && sourceFileFromMetadata.getFullText() === content) { 349 this._sourceFiles.set(canonicalFileName, sourceFileFromMetadata); 350 return sourceFileFromMetadata; 351 } 352 } 353 354 const parsed = ts.createSourceFile(fileName, content, languageVersion, this._setParentNodes || this.shouldAssertInvariants); 355 if (this.shouldAssertInvariants) { 356 Utils.assertInvariants(parsed, /*parent*/ undefined); 357 } 358 359 this._sourceFiles.set(canonicalFileName, parsed); 360 361 if (cacheKey) { 362 // store the cached source file on the unshadowed file with the same version. 363 const stats = this.vfs.statSync(canonicalFileName); 364 365 let fs = this.vfs; 366 while (fs.shadowRoot) { 367 try { 368 const shadowRootStats = fs.shadowRoot.existsSync(canonicalFileName) ? fs.shadowRoot.statSync(canonicalFileName) : undefined!; // TODO: GH#18217 369 if (shadowRootStats.dev !== stats.dev || 370 shadowRootStats.ino !== stats.ino || 371 shadowRootStats.mtimeMs !== stats.mtimeMs) { 372 break; 373 } 374 375 fs = fs.shadowRoot; 376 } 377 catch { 378 break; 379 } 380 } 381 382 if (fs !== this.vfs) { 383 fs.filemeta(canonicalFileName).set(cacheKey, parsed); 384 } 385 } 386 387 return parsed; 388 } 389 } 390 391 export type ExpectedDiagnosticMessage = [ts.DiagnosticMessage, ...(string | number)[]]; 392 export interface ExpectedDiagnosticMessageChain { 393 message: ExpectedDiagnosticMessage; 394 next?: ExpectedDiagnosticMessageChain[]; 395 } 396 397 export interface ExpectedDiagnosticLocation { 398 file: string; 399 start: number; 400 length: number; 401 } 402 export interface ExpectedDiagnosticRelatedInformation extends ExpectedDiagnosticMessageChain { 403 location?: ExpectedDiagnosticLocation; 404 } 405 406 export enum DiagnosticKind { 407 Error = "Error", 408 Status = "Status" 409 } 410 export interface ExpectedErrorDiagnostic extends ExpectedDiagnosticRelatedInformation { 411 relatedInformation?: ExpectedDiagnosticRelatedInformation[]; 412 } 413 414 export type ExpectedDiagnostic = ExpectedDiagnosticMessage | ExpectedErrorDiagnostic; 415 416 interface SolutionBuilderDiagnostic { 417 kind: DiagnosticKind; 418 diagnostic: ts.Diagnostic; 419 } 420 421 function indentedText(indent: number, text: string) { 422 if (!indent) return text; 423 let indentText = ""; 424 for (let i = 0; i < indent; i++) { 425 indentText += " "; 426 } 427 return ` 428${indentText}${text}`; 429 } 430 431 function expectedDiagnosticMessageToText([message, ...args]: ExpectedDiagnosticMessage) { 432 let text = ts.getLocaleSpecificMessage(message); 433 if (args.length) { 434 text = ts.formatStringFromArgs(text, args); 435 } 436 return text; 437 } 438 439 function expectedDiagnosticMessageChainToText({ message, next }: ExpectedDiagnosticMessageChain, indent = 0) { 440 let text = indentedText(indent, expectedDiagnosticMessageToText(message)); 441 if (next) { 442 indent++; 443 next.forEach(kid => text += expectedDiagnosticMessageChainToText(kid, indent)); 444 } 445 return text; 446 } 447 448 function expectedDiagnosticRelatedInformationToText({ location, ...diagnosticMessage }: ExpectedDiagnosticRelatedInformation) { 449 const text = expectedDiagnosticMessageChainToText(diagnosticMessage); 450 if (location) { 451 const { file, start, length } = location; 452 return `${file}(${start}:${length}):: ${text}`; 453 } 454 return text; 455 } 456 457 function expectedErrorDiagnosticToText({ relatedInformation, ...diagnosticRelatedInformation }: ExpectedErrorDiagnostic) { 458 let text = `${DiagnosticKind.Error}!: ${expectedDiagnosticRelatedInformationToText(diagnosticRelatedInformation)}`; 459 if (relatedInformation) { 460 for (const kid of relatedInformation) { 461 text += ` 462 related:: ${expectedDiagnosticRelatedInformationToText(kid)}`; 463 } 464 } 465 return text; 466 } 467 468 function expectedDiagnosticToText(errorOrStatus: ExpectedDiagnostic) { 469 return ts.isArray(errorOrStatus) ? 470 `${DiagnosticKind.Status}!: ${expectedDiagnosticMessageToText(errorOrStatus)}` : 471 expectedErrorDiagnosticToText(errorOrStatus); 472 } 473 474 function diagnosticMessageChainToText({ messageText, next}: ts.DiagnosticMessageChain, indent = 0) { 475 let text = indentedText(indent, messageText); 476 if (next) { 477 indent++; 478 next.forEach(kid => text += diagnosticMessageChainToText(kid, indent)); 479 } 480 return text; 481 } 482 483 function diagnosticRelatedInformationToText({ file, start, length, messageText }: ts.DiagnosticRelatedInformation) { 484 const text = typeof messageText === "string" ? 485 messageText : 486 diagnosticMessageChainToText(messageText); 487 return file ? 488 `${file.fileName}(${start}:${length}):: ${text}` : 489 text; 490 } 491 492 function diagnosticToText({ kind, diagnostic: { relatedInformation, ...diagnosticRelatedInformation } }: SolutionBuilderDiagnostic) { 493 let text = `${kind}!: ${diagnosticRelatedInformationToText(diagnosticRelatedInformation)}`; 494 if (relatedInformation) { 495 for (const kid of relatedInformation) { 496 text += ` 497 related:: ${diagnosticRelatedInformationToText(kid)}`; 498 } 499 } 500 return text; 501 } 502 503 export const version = "FakeTSVersion"; 504 505 export function patchHostForBuildInfoReadWrite<T extends ts.System>(sys: T) { 506 const originalReadFile = sys.readFile; 507 sys.readFile = (path, encoding) => { 508 const value = originalReadFile.call(sys, path, encoding); 509 if (!value || !ts.isBuildInfoFile(path)) return value; 510 const buildInfo = ts.getBuildInfo(path, value); 511 if (!buildInfo) return value; 512 ts.Debug.assert(buildInfo.version === version); 513 buildInfo.version = ts.version; 514 return ts.getBuildInfoText(buildInfo); 515 }; 516 return patchHostForBuildInfoWrite(sys, version); 517 } 518 519 export function patchHostForBuildInfoWrite<T extends ts.System>(sys: T, version: string) { 520 const originalWrite = sys.write; 521 sys.write = msg => originalWrite.call(sys, msg.replace(ts.version, version)); 522 const originalWriteFile = sys.writeFile; 523 sys.writeFile = (fileName: string, content: string, writeByteOrderMark: boolean) => { 524 if (ts.isBuildInfoFile(fileName)) { 525 const buildInfo = ts.getBuildInfo(fileName, content); 526 if (buildInfo) { 527 buildInfo.version = version; 528 return originalWriteFile.call(sys, fileName, ts.getBuildInfoText(buildInfo), writeByteOrderMark); 529 } 530 } 531 return originalWriteFile.call(sys, fileName, content, writeByteOrderMark); 532 }; 533 return sys; 534 } 535 536 export class SolutionBuilderHost extends CompilerHost implements ts.SolutionBuilderHost<ts.BuilderProgram> { 537 createProgram: ts.CreateProgram<ts.BuilderProgram>; 538 539 private constructor(sys: System | vfs.FileSystem, options?: ts.CompilerOptions, setParentNodes?: boolean, createProgram?: ts.CreateProgram<ts.BuilderProgram>) { 540 super(sys, options, setParentNodes); 541 this.createProgram = createProgram || ts.createEmitAndSemanticDiagnosticsBuilderProgram; 542 } 543 544 static create(sys: System | vfs.FileSystem, options?: ts.CompilerOptions, setParentNodes?: boolean, createProgram?: ts.CreateProgram<ts.BuilderProgram>) { 545 const host = new SolutionBuilderHost(sys, options, setParentNodes, createProgram); 546 patchHostForBuildInfoReadWrite(host.sys); 547 return host; 548 } 549 550 createHash(data: string) { 551 return `${ts.generateDjb2Hash(data)}-${data}`; 552 } 553 554 diagnostics: SolutionBuilderDiagnostic[] = []; 555 556 reportDiagnostic(diagnostic: ts.Diagnostic) { 557 this.diagnostics.push({ kind: DiagnosticKind.Error, diagnostic }); 558 } 559 560 reportSolutionBuilderStatus(diagnostic: ts.Diagnostic) { 561 this.diagnostics.push({ kind: DiagnosticKind.Status, diagnostic }); 562 } 563 564 clearDiagnostics() { 565 this.diagnostics.length = 0; 566 } 567 568 assertDiagnosticMessages(...expectedDiagnostics: ExpectedDiagnostic[]) { 569 const actual = this.diagnostics.slice().map(diagnosticToText); 570 const expected = expectedDiagnostics.map(expectedDiagnosticToText); 571 assert.deepEqual(actual, expected, `Diagnostic arrays did not match: 572Actual: ${JSON.stringify(actual, /*replacer*/ undefined, " ")} 573Expected: ${JSON.stringify(expected, /*replacer*/ undefined, " ")}`); 574 } 575 576 assertErrors(...expectedDiagnostics: ExpectedErrorDiagnostic[]) { 577 const actual = this.diagnostics.filter(d => d.kind === DiagnosticKind.Error).map(diagnosticToText); 578 const expected = expectedDiagnostics.map(expectedDiagnosticToText); 579 assert.deepEqual(actual, expected, `Diagnostics arrays did not match: 580Actual: ${JSON.stringify(actual, /*replacer*/ undefined, " ")} 581Expected: ${JSON.stringify(expected, /*replacer*/ undefined, " ")} 582Actual All:: ${JSON.stringify(this.diagnostics.slice().map(diagnosticToText), /*replacer*/ undefined, " ")}`); 583 } 584 585 printDiagnostics(header = "== Diagnostics ==") { 586 const out = ts.createDiagnosticReporter(ts.sys); 587 ts.sys.write(header + "\r\n"); 588 for (const { diagnostic } of this.diagnostics) { 589 out(diagnostic); 590 } 591 } 592 593 now() { 594 return this.sys.now(); 595 } 596 } 597} 598 599