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