1namespace Harness { 2 export interface IO { 3 newLine(): string; 4 getCurrentDirectory(): string; 5 useCaseSensitiveFileNames(): boolean; 6 resolvePath(path: string): string | undefined; 7 getFileSize(path: string): number; 8 readFile(path: string): string | undefined; 9 writeFile(path: string, contents: string): void; 10 directoryName(path: string): string | undefined; 11 getDirectories(path: string): string[]; 12 createDirectory(path: string): void; 13 fileExists(fileName: string): boolean; 14 directoryExists(path: string): boolean; 15 deleteFile(fileName: string): void; 16 enumerateTestFiles(runner: RunnerBase): (string | FileBasedTest)[]; 17 listFiles(path: string, filter?: RegExp, options?: { recursive?: boolean }): string[]; 18 log(text: string): void; 19 args(): string[]; 20 getExecutingFilePath(): string; 21 getWorkspaceRoot(): string; 22 exit(exitCode?: number): void; 23 readDirectory(path: string, extension?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): readonly string[]; 24 getAccessibleFileSystemEntries(dirname: string): ts.FileSystemEntries; 25 tryEnableSourceMapsForHost?(): void; 26 getEnvironmentVariable?(name: string): string; 27 getMemoryUsage?(): number | undefined; 28 } 29 30 export let IO: IO; 31 export function setHarnessIO(io: IO) { 32 IO = io; 33 } 34 35 // harness always uses one kind of new line 36 // But note that `parseTestData` in `fourslash.ts` uses "\n" 37 export const harnessNewLine = "\r\n"; 38 39 // Root for file paths that are stored in a virtual file system 40 export const virtualFileSystemRoot = "/"; 41 42 function createNodeIO(): IO { 43 let fs: any, pathModule: any; 44 if (require) { 45 fs = require("fs"); 46 pathModule = require("path"); 47 } 48 else { 49 fs = pathModule = {}; 50 } 51 52 function deleteFile(path: string) { 53 try { 54 fs.unlinkSync(path); 55 } 56 catch { /*ignore*/ } 57 } 58 59 function directoryName(path: string) { 60 const dirPath = pathModule.dirname(path); 61 // Node will just continue to repeat the root path, rather than return null 62 return dirPath === path ? undefined : dirPath; 63 } 64 65 function enumerateTestFiles(runner: RunnerBase) { 66 return runner.getTestFiles(); 67 } 68 69 function listFiles(path: string, spec: RegExp, options: { recursive?: boolean } = {}) { 70 function filesInFolder(folder: string): string[] { 71 let paths: string[] = []; 72 73 for (const file of fs.readdirSync(folder)) { 74 const pathToFile = pathModule.join(folder, file); 75 const stat = fs.statSync(pathToFile); 76 if (options.recursive && stat.isDirectory()) { 77 paths = paths.concat(filesInFolder(pathToFile)); 78 } 79 else if (stat.isFile() && (!spec || file.match(spec))) { 80 paths.push(pathToFile); 81 } 82 } 83 84 return paths; 85 } 86 87 return filesInFolder(path); 88 } 89 90 function getAccessibleFileSystemEntries(dirname: string): ts.FileSystemEntries { 91 try { 92 const entries: string[] = fs.readdirSync(dirname || ".").sort(ts.sys.useCaseSensitiveFileNames ? ts.compareStringsCaseSensitive : ts.compareStringsCaseInsensitive); 93 const files: string[] = []; 94 const directories: string[] = []; 95 for (const entry of entries) { 96 if (entry === "." || entry === "..") continue; 97 const name = vpath.combine(dirname, entry); 98 try { 99 const stat = fs.statSync(name); 100 if (!stat) continue; 101 if (stat.isFile()) { 102 files.push(entry); 103 } 104 else if (stat.isDirectory()) { 105 directories.push(entry); 106 } 107 } 108 catch { /*ignore*/ } 109 } 110 return { files, directories }; 111 } 112 catch (e) { 113 return { files: [], directories: [] }; 114 } 115 } 116 117 function createDirectory(path: string) { 118 try { 119 fs.mkdirSync(path); 120 } 121 catch (e) { 122 if (e.code === "ENOENT") { 123 createDirectory(vpath.dirname(path)); 124 createDirectory(path); 125 } 126 else if (!ts.sys.directoryExists(path)) { 127 throw e; 128 } 129 } 130 } 131 132 return { 133 newLine: () => harnessNewLine, 134 getCurrentDirectory: () => ts.sys.getCurrentDirectory(), 135 useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames, 136 resolvePath: (path: string) => ts.sys.resolvePath(path), 137 getFileSize: (path: string) => ts.sys.getFileSize!(path), 138 readFile: path => ts.sys.readFile(path), 139 writeFile: (path, content) => ts.sys.writeFile(path, content), 140 directoryName, 141 getDirectories: path => ts.sys.getDirectories(path), 142 createDirectory, 143 fileExists: path => ts.sys.fileExists(path), 144 directoryExists: path => ts.sys.directoryExists(path), 145 deleteFile, 146 listFiles, 147 enumerateTestFiles, 148 log: s => console.log(s), 149 args: () => ts.sys.args, 150 getExecutingFilePath: () => ts.sys.getExecutingFilePath(), 151 getWorkspaceRoot: () => vpath.resolve(__dirname, "../.."), 152 exit: exitCode => ts.sys.exit(exitCode), 153 readDirectory: (path, extension, exclude, include, depth) => ts.sys.readDirectory(path, extension, exclude, include, depth), 154 getAccessibleFileSystemEntries, 155 tryEnableSourceMapsForHost: () => ts.sys.tryEnableSourceMapsForHost && ts.sys.tryEnableSourceMapsForHost(), 156 getMemoryUsage: () => ts.sys.getMemoryUsage && ts.sys.getMemoryUsage(), 157 getEnvironmentVariable: name => ts.sys.getEnvironmentVariable(name), 158 }; 159 } 160 161 export function mockHash(s: string): string { 162 return `hash-${s}`; 163 } 164 165 IO = createNodeIO(); 166 167 if (IO.tryEnableSourceMapsForHost && /^development$/i.test(IO.getEnvironmentVariable!("NODE_ENV"))) { 168 IO.tryEnableSourceMapsForHost(); 169 } 170 171 export const libFolder = "built/local/"; 172 const tcServicesFileName = ts.combinePaths(libFolder, "typescriptServices.js"); 173 export const tcServicesFile = IO.readFile(tcServicesFileName) + IO.newLine() + `//# sourceURL=${IO.resolvePath(tcServicesFileName)}`; 174 175 export type SourceMapEmitterCallback = ( 176 emittedFile: string, 177 emittedLine: number, 178 emittedColumn: number, 179 sourceFile: string, 180 sourceLine: number, 181 sourceColumn: number, 182 sourceName: string, 183 ) => void; 184 185 // Settings 186 /* eslint-disable prefer-const */ 187 export let userSpecifiedRoot = ""; 188 export let lightMode = false; 189 /* eslint-enable prefer-const */ 190 export function setLightMode(flag: boolean) { 191 lightMode = flag; 192 } 193 194 /** Functionality for compiling TypeScript code */ 195 export namespace Compiler { 196 /** Aggregate various writes into a single array of lines. Useful for passing to the 197 * TypeScript compiler to fill with source code or errors. 198 */ 199 export class WriterAggregator { 200 public lines: string[] = []; 201 public currentLine: string = undefined!; 202 203 public Write(str: string) { 204 // out of memory usage concerns avoid using + or += if we're going to do any manipulation of this string later 205 this.currentLine = [(this.currentLine || ""), str].join(""); 206 } 207 208 public WriteLine(str: string) { 209 // out of memory usage concerns avoid using + or += if we're going to do any manipulation of this string later 210 this.lines.push([(this.currentLine || ""), str].join("")); 211 this.currentLine = undefined!; 212 } 213 214 public Close() { 215 if (this.currentLine !== undefined) { this.lines.push(this.currentLine); } 216 this.currentLine = undefined!; 217 } 218 219 public reset() { 220 this.lines = []; 221 this.currentLine = undefined!; 222 } 223 } 224 225 export function createSourceFileAndAssertInvariants( 226 fileName: string, 227 sourceText: string, 228 languageVersion: ts.ScriptTarget) { 229 // We'll only assert invariants outside of light mode. 230 const shouldAssertInvariants = !lightMode; 231 232 // Only set the parent nodes if we're asserting invariants. We don't need them otherwise. 233 const result = ts.createSourceFile(fileName, sourceText, languageVersion, /*setParentNodes:*/ shouldAssertInvariants); 234 235 if (shouldAssertInvariants) { 236 Utils.assertInvariants(result, /*parent:*/ undefined); 237 } 238 239 return result; 240 } 241 242 export const defaultLibFileName = "lib.d.ts"; 243 export const es2015DefaultLibFileName = "lib.es2015.d.ts"; 244 245 // Cache of lib files from "built/local" 246 let libFileNameSourceFileMap: ts.ESMap<string, ts.SourceFile> | undefined; 247 248 export function getDefaultLibrarySourceFile(fileName = defaultLibFileName): ts.SourceFile | undefined { 249 if (!isDefaultLibraryFile(fileName)) { 250 return undefined; 251 } 252 253 if (!libFileNameSourceFileMap) { 254 libFileNameSourceFileMap = new ts.Map(ts.getEntries({ 255 [defaultLibFileName]: createSourceFileAndAssertInvariants(defaultLibFileName, IO.readFile(libFolder + "lib.es5.d.ts")!, /*languageVersion*/ ts.ScriptTarget.Latest) 256 })); 257 } 258 259 let sourceFile = libFileNameSourceFileMap.get(fileName); 260 if (!sourceFile) { 261 libFileNameSourceFileMap.set(fileName, sourceFile = createSourceFileAndAssertInvariants(fileName, IO.readFile(libFolder + fileName)!, ts.ScriptTarget.Latest)); 262 } 263 return sourceFile; 264 } 265 266 export function getDefaultLibFileName(options: ts.CompilerOptions): string { 267 switch (options.target) { 268 case ts.ScriptTarget.ESNext: 269 case ts.ScriptTarget.ES2017: 270 return "lib.es2017.d.ts"; 271 case ts.ScriptTarget.ES2016: 272 return "lib.es2016.d.ts"; 273 case ts.ScriptTarget.ES2015: 274 return es2015DefaultLibFileName; 275 276 default: 277 return defaultLibFileName; 278 } 279 } 280 281 // Cache these between executions so we don't have to re-parse them for every test 282 export const fourslashFileName = "fourslash.ts"; 283 export let fourslashSourceFile: ts.SourceFile; 284 285 export function getCanonicalFileName(fileName: string): string { 286 return fileName; 287 } 288 289 interface HarnessOptions { 290 useCaseSensitiveFileNames?: boolean; 291 includeBuiltFile?: string; 292 baselineFile?: string; 293 libFiles?: string; 294 noTypesAndSymbols?: boolean; 295 } 296 297 // Additional options not already in ts.optionDeclarations 298 const harnessOptionDeclarations: ts.CommandLineOption[] = [ 299 { name: "allowNonTsExtensions", type: "boolean" }, 300 { name: "useCaseSensitiveFileNames", type: "boolean" }, 301 { name: "baselineFile", type: "string" }, 302 { name: "includeBuiltFile", type: "string" }, 303 { name: "fileName", type: "string" }, 304 { name: "libFiles", type: "string" }, 305 { name: "noErrorTruncation", type: "boolean" }, 306 { name: "suppressOutputPathCheck", type: "boolean" }, 307 { name: "noImplicitReferences", type: "boolean" }, 308 { name: "currentDirectory", type: "string" }, 309 { name: "symlink", type: "string" }, 310 { name: "link", type: "string" }, 311 { name: "noTypesAndSymbols", type: "boolean" }, 312 // Emitted js baseline will print full paths for every output file 313 { name: "fullEmitPaths", type: "boolean" } 314 ]; 315 316 let optionsIndex: ts.ESMap<string, ts.CommandLineOption>; 317 function getCommandLineOption(name: string): ts.CommandLineOption | undefined { 318 if (!optionsIndex) { 319 optionsIndex = new ts.Map<string, ts.CommandLineOption>(); 320 const optionDeclarations = harnessOptionDeclarations.concat(ts.optionDeclarations); 321 for (const option of optionDeclarations) { 322 optionsIndex.set(option.name.toLowerCase(), option); 323 } 324 } 325 return optionsIndex.get(name.toLowerCase()); 326 } 327 328 export function setCompilerOptionsFromHarnessSetting(settings: TestCaseParser.CompilerSettings, options: ts.CompilerOptions & HarnessOptions): void { 329 for (const name in settings) { 330 if (settings.hasOwnProperty(name)) { 331 const value = settings[name]; 332 if (value === undefined) { 333 throw new Error(`Cannot have undefined value for compiler option '${name}'.`); 334 } 335 const option = getCommandLineOption(name); 336 if (option) { 337 const errors: ts.Diagnostic[] = []; 338 options[option.name] = optionValue(option, value, errors); 339 if (errors.length > 0) { 340 throw new Error(`Unknown value '${value}' for compiler option '${name}'.`); 341 } 342 } 343 else { 344 throw new Error(`Unknown compiler option '${name}'.`); 345 } 346 } 347 } 348 } 349 350 function optionValue(option: ts.CommandLineOption, value: string, errors: ts.Diagnostic[]): any { 351 switch (option.type) { 352 case "boolean": 353 return value.toLowerCase() === "true"; 354 case "string": 355 return value; 356 case "number": { 357 const numverValue = parseInt(value, 10); 358 if (isNaN(numverValue)) { 359 throw new Error(`Value must be a number, got: ${JSON.stringify(value)}`); 360 } 361 return numverValue; 362 } 363 // If not a primitive, the possible types are specified in what is effectively a map of options. 364 case "list": 365 return ts.parseListTypeOption(option, value, errors); 366 default: 367 if (option.name === "ets") { 368 const etsOptionFilePath = IO.resolvePath("tests/cases/fourslash/etsOption.json"); 369 const etsOptionJson = IO.readFile(etsOptionFilePath!); 370 const etsOption = <ts.EtsOptions>JSON.parse(etsOptionJson!); 371 const etsLibs: string[] = []; 372 etsOption?.libs?.forEach(filename => { 373 const absuluteFilePath = IO.resolvePath(filename); 374 etsLibs.push(absuluteFilePath!); 375 }); 376 etsOption.libs = etsLibs; 377 return etsOption; 378 } 379 return ts.parseCustomTypeOption(<ts.CommandLineOptionOfCustomType>option, value, errors); 380 } 381 } 382 383 export interface TestFile { 384 unitName: string; 385 content: string; 386 fileOptions?: any; 387 } 388 389 export function compileFiles( 390 inputFiles: TestFile[], 391 otherFiles: TestFile[], 392 harnessSettings: TestCaseParser.CompilerSettings | undefined, 393 compilerOptions: ts.CompilerOptions | undefined, 394 // Current directory is needed for rwcRunner to be able to use currentDirectory defined in json file 395 currentDirectory: string | undefined, 396 symlinks?: vfs.FileSet 397 ): compiler.CompilationResult { 398 const options: ts.CompilerOptions & HarnessOptions = compilerOptions ? ts.cloneCompilerOptions(compilerOptions) : { noResolve: false }; 399 options.target = options.target || ts.ScriptTarget.ES3; 400 options.newLine = options.newLine || ts.NewLineKind.CarriageReturnLineFeed; 401 options.noErrorTruncation = true; 402 options.skipDefaultLibCheck = typeof options.skipDefaultLibCheck === "undefined" ? true : options.skipDefaultLibCheck; 403 404 if (typeof currentDirectory === "undefined") { 405 currentDirectory = vfs.srcFolder; 406 } 407 408 // Parse settings 409 if (harnessSettings) { 410 setCompilerOptionsFromHarnessSetting(harnessSettings, options); 411 } 412 if (options.rootDirs) { 413 options.rootDirs = ts.map(options.rootDirs, d => ts.getNormalizedAbsolutePath(d, currentDirectory)); 414 } 415 416 const useCaseSensitiveFileNames = options.useCaseSensitiveFileNames !== undefined ? options.useCaseSensitiveFileNames : true; 417 const programFileNames = inputFiles.map(file => file.unitName).filter(fileName => !ts.fileExtensionIs(fileName, ts.Extension.Json) && !ts.fileExtensionIs(fileName, ".json5")); 418 419 // Files from built\local that are requested by test "@includeBuiltFiles" to be in the context. 420 // Treat them as library files, so include them in build, but not in baselines. 421 if (options.includeBuiltFile) { 422 programFileNames.push(vpath.combine(vfs.builtFolder, options.includeBuiltFile)); 423 } 424 425 // Files from tests\lib that are requested by "@libFiles" 426 if (options.libFiles) { 427 for (const fileName of options.libFiles.split(",")) { 428 programFileNames.push(vpath.combine(vfs.testLibFolder, fileName)); 429 } 430 } 431 432 const docs = inputFiles.concat(otherFiles).map(documents.TextDocument.fromTestFile); 433 const fs = vfs.createFromFileSystem(IO, !useCaseSensitiveFileNames, { documents: docs, cwd: currentDirectory }); 434 if (symlinks) { 435 fs.apply(symlinks); 436 } 437 const host = new fakes.CompilerHost(fs, options); 438 const result = compiler.compileFiles(host, programFileNames, options); 439 result.symlinks = symlinks; 440 return result; 441 } 442 443 export interface DeclarationCompilationContext { 444 declInputFiles: TestFile[]; 445 declOtherFiles: TestFile[]; 446 harnessSettings: TestCaseParser.CompilerSettings & HarnessOptions | undefined; 447 options: ts.CompilerOptions; 448 currentDirectory: string; 449 } 450 451 export function prepareDeclarationCompilationContext(inputFiles: readonly TestFile[], 452 otherFiles: readonly TestFile[], 453 result: compiler.CompilationResult, 454 harnessSettings: TestCaseParser.CompilerSettings & HarnessOptions, 455 options: ts.CompilerOptions, 456 // Current directory is needed for rwcRunner to be able to use currentDirectory defined in json file 457 currentDirectory: string | undefined): DeclarationCompilationContext | undefined { 458 459 if (options.declaration && result.diagnostics.length === 0) { 460 if (options.emitDeclarationOnly) { 461 if (result.js.size > 0 || result.dts.size === 0) { 462 throw new Error("Only declaration files should be generated when emitDeclarationOnly:true"); 463 } 464 } 465 else if (result.dts.size !== result.getNumberOfJsFiles(/*includeJson*/ false)) { 466 throw new Error("There were no errors and declFiles generated did not match number of js files generated"); 467 } 468 } 469 470 const declInputFiles: TestFile[] = []; 471 const declOtherFiles: TestFile[] = []; 472 473 // if the .d.ts is non-empty, confirm it compiles correctly as well 474 if (options.declaration && result.diagnostics.length === 0 && result.dts.size > 0) { 475 ts.forEach(inputFiles, file => addDtsFile(file, declInputFiles)); 476 ts.forEach(otherFiles, file => addDtsFile(file, declOtherFiles)); 477 return { declInputFiles, declOtherFiles, harnessSettings, options, currentDirectory: currentDirectory || harnessSettings.currentDirectory }; 478 } 479 480 function addDtsFile(file: TestFile, dtsFiles: TestFile[]) { 481 if (vpath.isDeclaration(file.unitName) || vpath.isJson(file.unitName)) { 482 dtsFiles.push(file); 483 } 484 else if (vpath.isTypeScript(file.unitName) || (vpath.isJavaScript(file.unitName) && ts.getAllowJSCompilerOption(options))) { 485 const declFile = findResultCodeFile(file.unitName); 486 if (declFile && !findUnit(declFile.file, declInputFiles) && !findUnit(declFile.file, declOtherFiles)) { 487 dtsFiles.push({ unitName: declFile.file, content: Utils.removeByteOrderMark(declFile.text) }); 488 } 489 } 490 } 491 492 function findResultCodeFile(fileName: string) { 493 const sourceFile = result.program!.getSourceFile(fileName)!; 494 assert(sourceFile, "Program has no source file with name '" + fileName + "'"); 495 // Is this file going to be emitted separately 496 let sourceFileName: string; 497 const outFile = options.outFile || options.out; 498 if (!outFile) { 499 if (options.outDir) { 500 let sourceFilePath = ts.getNormalizedAbsolutePath(sourceFile.fileName, result.vfs.cwd()); 501 sourceFilePath = sourceFilePath.replace(result.program!.getCommonSourceDirectory(), ""); 502 sourceFileName = ts.combinePaths(options.outDir, sourceFilePath); 503 } 504 else { 505 sourceFileName = sourceFile.fileName; 506 } 507 } 508 else { 509 // Goes to single --out file 510 sourceFileName = outFile; 511 } 512 513 const dTsFileName = ts.removeFileExtension(sourceFileName) + ts.Extension.Dts; 514 return result.dts.get(dTsFileName); 515 } 516 517 function findUnit(fileName: string, units: TestFile[]) { 518 return ts.forEach(units, unit => unit.unitName === fileName ? unit : undefined); 519 } 520 } 521 522 export function compileDeclarationFiles(context: DeclarationCompilationContext | undefined, symlinks: vfs.FileSet | undefined) { 523 if (!context) { 524 return; 525 } 526 const { declInputFiles, declOtherFiles, harnessSettings, options, currentDirectory } = context; 527 const output = compileFiles(declInputFiles, declOtherFiles, harnessSettings, options, currentDirectory, symlinks); 528 return { declInputFiles, declOtherFiles, declResult: output }; 529 } 530 531 export function minimalDiagnosticsToString(diagnostics: readonly ts.Diagnostic[], pretty?: boolean) { 532 const host = { getCanonicalFileName, getCurrentDirectory: () => "", getNewLine: () => IO.newLine() }; 533 return (pretty ? ts.formatDiagnosticsWithColorAndContext : ts.formatDiagnostics)(diagnostics, host); 534 } 535 536 export function getErrorBaseline(inputFiles: readonly TestFile[], diagnostics: readonly ts.Diagnostic[], pretty?: boolean) { 537 let outputLines = ""; 538 const gen = iterateErrorBaseline(inputFiles, diagnostics, { pretty }); 539 for (let {done, value} = gen.next(); !done; { done, value } = gen.next()) { 540 const [, content] = value; 541 outputLines += content; 542 } 543 if (pretty) { 544 outputLines += ts.getErrorSummaryText(ts.getErrorCountForSummary(diagnostics), IO.newLine()); 545 } 546 return outputLines; 547 } 548 549 export const diagnosticSummaryMarker = "__diagnosticSummary"; 550 export const globalErrorsMarker = "__globalErrors"; 551 export function *iterateErrorBaseline(inputFiles: readonly TestFile[], diagnostics: readonly ts.Diagnostic[], options?: { pretty?: boolean, caseSensitive?: boolean, currentDirectory?: string }): IterableIterator<[string, string, number]> { 552 diagnostics = ts.sort(diagnostics, ts.compareDiagnostics); 553 let outputLines = ""; 554 // Count up all errors that were found in files other than lib.d.ts so we don't miss any 555 let totalErrorsReportedInNonLibraryFiles = 0; 556 557 let errorsReported = 0; 558 559 let firstLine = true; 560 function newLine() { 561 if (firstLine) { 562 firstLine = false; 563 return ""; 564 } 565 return "\r\n"; 566 } 567 568 const formatDiagnsoticHost = { 569 getCurrentDirectory: () => options && options.currentDirectory ? options.currentDirectory : "", 570 getNewLine: () => IO.newLine(), 571 getCanonicalFileName: ts.createGetCanonicalFileName(options && options.caseSensitive !== undefined ? options.caseSensitive : true), 572 }; 573 574 function outputErrorText(error: ts.Diagnostic) { 575 const message = ts.flattenDiagnosticMessageText(error.messageText, IO.newLine()); 576 577 const errLines = Utils.removeTestPathPrefixes(message) 578 .split("\n") 579 .map(s => s.length > 0 && s.charAt(s.length - 1) === "\r" ? s.substr(0, s.length - 1) : s) 580 .filter(s => s.length > 0) 581 .map(s => "!!! " + ts.diagnosticCategoryName(error) + " TS" + error.code + ": " + s); 582 if (error.relatedInformation) { 583 for (const info of error.relatedInformation) { 584 errLines.push(`!!! related TS${info.code}${info.file ? " " + ts.formatLocation(info.file, info.start!, formatDiagnsoticHost, ts.identity) : ""}: ${ts.flattenDiagnosticMessageText(info.messageText, IO.newLine())}`); 585 } 586 } 587 errLines.forEach(e => outputLines += (newLine() + e)); 588 errorsReported++; 589 590 // do not count errors from lib.d.ts here, they are computed separately as numLibraryDiagnostics 591 // if lib.d.ts is explicitly included in input files and there are some errors in it (i.e. because of duplicate identifiers) 592 // then they will be added twice thus triggering 'total errors' assertion with condition 593 // 'totalErrorsReportedInNonLibraryFiles + numLibraryDiagnostics + numTest262HarnessDiagnostics, diagnostics.length 594 595 if (!error.file || !isDefaultLibraryFile(error.file.fileName)) { 596 totalErrorsReportedInNonLibraryFiles++; 597 } 598 } 599 600 yield [diagnosticSummaryMarker, Utils.removeTestPathPrefixes(minimalDiagnosticsToString(diagnostics, options && options.pretty)) + IO.newLine() + IO.newLine(), diagnostics.length]; 601 602 // Report global errors 603 const globalErrors = diagnostics.filter(err => !err.file); 604 globalErrors.forEach(outputErrorText); 605 yield [globalErrorsMarker, outputLines, errorsReported]; 606 outputLines = ""; 607 errorsReported = 0; 608 609 // 'merge' the lines of each input file with any errors associated with it 610 const dupeCase = new ts.Map<string, number>(); 611 for (const inputFile of inputFiles.filter(f => f.content !== undefined)) { 612 // Filter down to the errors in the file 613 const fileErrors = diagnostics.filter((e): e is ts.DiagnosticWithLocation => { 614 const errFn = e.file; 615 return !!errFn && ts.comparePaths(Utils.removeTestPathPrefixes(errFn.fileName), Utils.removeTestPathPrefixes(inputFile.unitName), options && options.currentDirectory || "", !(options && options.caseSensitive)) === ts.Comparison.EqualTo; 616 }); 617 618 619 // Header 620 outputLines += (newLine() + "==== " + inputFile.unitName + " (" + fileErrors.length + " errors) ===="); 621 622 // Make sure we emit something for every error 623 let markedErrorCount = 0; 624 // For each line, emit the line followed by any error squiggles matching this line 625 // Note: IE JS engine incorrectly handles consecutive delimiters here when using RegExp split, so 626 // we have to string-based splitting instead and try to figure out the delimiting chars 627 628 const lineStarts = ts.computeLineStarts(inputFile.content); 629 let lines = inputFile.content.split("\n"); 630 if (lines.length === 1) { 631 lines = lines[0].split("\r"); 632 } 633 634 lines.forEach((line, lineIndex) => { 635 if (line.length > 0 && line.charAt(line.length - 1) === "\r") { 636 line = line.substr(0, line.length - 1); 637 } 638 639 const thisLineStart = lineStarts[lineIndex]; 640 let nextLineStart: number; 641 // On the last line of the file, fake the next line start number so that we handle errors on the last character of the file correctly 642 if (lineIndex === lines.length - 1) { 643 nextLineStart = inputFile.content.length; 644 } 645 else { 646 nextLineStart = lineStarts[lineIndex + 1]; 647 } 648 // Emit this line from the original file 649 outputLines += (newLine() + " " + line); 650 fileErrors.forEach(errDiagnostic => { 651 const err = errDiagnostic as ts.TextSpan; // TODO: GH#18217 652 // Does any error start or continue on to this line? Emit squiggles 653 const end = ts.textSpanEnd(err); 654 if ((end >= thisLineStart) && ((err.start < nextLineStart) || (lineIndex === lines.length - 1))) { 655 // How many characters from the start of this line the error starts at (could be positive or negative) 656 const relativeOffset = err.start - thisLineStart; 657 // How many characters of the error are on this line (might be longer than this line in reality) 658 const length = (end - err.start) - Math.max(0, thisLineStart - err.start); 659 // Calculate the start of the squiggle 660 const squiggleStart = Math.max(0, relativeOffset); 661 // TODO/REVIEW: this doesn't work quite right in the browser if a multi file test has files whose names are just the right length relative to one another 662 outputLines += (newLine() + " " + line.substr(0, squiggleStart).replace(/[^\s]/g, " ") + new Array(Math.min(length, line.length - squiggleStart) + 1).join("~")); 663 664 // If the error ended here, or we're at the end of the file, emit its message 665 if ((lineIndex === lines.length - 1) || nextLineStart > end) { 666 // Just like above, we need to do a split on a string instead of on a regex 667 // because the JS engine does regexes wrong 668 669 outputErrorText(errDiagnostic); 670 markedErrorCount++; 671 } 672 } 673 }); 674 }); 675 676 // Verify we didn't miss any errors in this file 677 assert.equal(markedErrorCount, fileErrors.length, "count of errors in " + inputFile.unitName); 678 const isDupe = dupeCase.has(sanitizeTestFilePath(inputFile.unitName)); 679 yield [checkDuplicatedFileName(inputFile.unitName, dupeCase), outputLines, errorsReported]; 680 if (isDupe && !(options && options.caseSensitive)) { 681 // Case-duplicated files on a case-insensitive build will have errors reported in both the dupe and the original 682 // thanks to the canse-insensitive path comparison on the error file path - We only want to count those errors once 683 // for the assert below, so we subtract them here. 684 totalErrorsReportedInNonLibraryFiles -= errorsReported; 685 } 686 outputLines = ""; 687 errorsReported = 0; 688 } 689 690 const numLibraryDiagnostics = ts.countWhere(diagnostics, diagnostic => { 691 return !!diagnostic.file && (isDefaultLibraryFile(diagnostic.file.fileName) || isBuiltFile(diagnostic.file.fileName)); 692 }); 693 694 const numTest262HarnessDiagnostics = ts.countWhere(diagnostics, diagnostic => { 695 // Count an error generated from tests262-harness folder.This should only apply for test262 696 return !!diagnostic.file && diagnostic.file.fileName.indexOf("test262-harness") >= 0; 697 }); 698 699 // Verify we didn't miss any errors in total 700 assert.equal(totalErrorsReportedInNonLibraryFiles + numLibraryDiagnostics + numTest262HarnessDiagnostics, diagnostics.length, "total number of errors"); 701 } 702 703 export function doErrorBaseline(baselinePath: string, inputFiles: readonly TestFile[], errors: readonly ts.Diagnostic[], pretty?: boolean) { 704 Baseline.runBaseline(baselinePath.replace(/\.tsx?$/, ".errors.txt"), 705 !errors || (errors.length === 0) ? null : getErrorBaseline(inputFiles, errors, pretty)); // eslint-disable-line no-null/no-null 706 } 707 708 export function doTypeAndSymbolBaseline(baselinePath: string, program: ts.Program, allFiles: {unitName: string, content: string}[], opts?: Baseline.BaselineOptions, multifile?: boolean, skipTypeBaselines?: boolean, skipSymbolBaselines?: boolean, hasErrorBaseline?: boolean) { 709 // The full walker simulates the types that you would get from doing a full 710 // compile. The pull walker simulates the types you get when you just do 711 // a type query for a random node (like how the LS would do it). Most of the 712 // time, these will be the same. However, occasionally, they can be different. 713 // Specifically, when the compiler internally depends on symbol IDs to order 714 // things, then we may see different results because symbols can be created in a 715 // different order with 'pull' operations, and thus can produce slightly differing 716 // output. 717 // 718 // For example, with a full type check, we may see a type displayed as: number | string 719 // But with a pull type check, we may see it as: string | number 720 // 721 // These types are equivalent, but depend on what order the compiler observed 722 // certain parts of the program. 723 724 const fullWalker = new TypeWriterWalker(program, /*fullTypeCheck*/ true, !!hasErrorBaseline); 725 726 // Produce baselines. The first gives the types for all expressions. 727 // The second gives symbols for all identifiers. 728 let typesError: Error | undefined, symbolsError: Error | undefined; 729 try { 730 checkBaseLines(/*isSymbolBaseLine*/ false); 731 } 732 catch (e) { 733 typesError = e; 734 } 735 736 try { 737 checkBaseLines(/*isSymbolBaseLine*/ true); 738 } 739 catch (e) { 740 symbolsError = e; 741 } 742 743 if (typesError && symbolsError) { 744 throw new Error(typesError.stack + IO.newLine() + symbolsError.stack); 745 } 746 747 if (typesError) { 748 throw typesError; 749 } 750 751 if (symbolsError) { 752 throw symbolsError; 753 } 754 755 return; 756 757 function checkBaseLines(isSymbolBaseLine: boolean) { 758 const fullExtension = isSymbolBaseLine ? ".symbols" : ".types"; 759 // When calling this function from rwc-runner, the baselinePath will have no extension. 760 // As rwc test- file is stored in json which ".json" will get stripped off. 761 // When calling this function from compiler-runner, the baselinePath will then has either ".ts" or ".tsx" extension 762 const outputFileName = ts.endsWith(baselinePath, ts.Extension.Ts) || ts.endsWith(baselinePath, ts.Extension.Tsx) ? 763 baselinePath.replace(/\.tsx?/, "") : baselinePath; 764 765 if (!multifile) { 766 const fullBaseLine = generateBaseLine(isSymbolBaseLine, isSymbolBaseLine ? skipSymbolBaselines : skipTypeBaselines); 767 Baseline.runBaseline(outputFileName + fullExtension, fullBaseLine, opts); 768 } 769 else { 770 Baseline.runMultifileBaseline(outputFileName, fullExtension, () => { 771 return iterateBaseLine(isSymbolBaseLine, isSymbolBaseLine ? skipSymbolBaselines : skipTypeBaselines); 772 }, opts); 773 } 774 } 775 776 function generateBaseLine(isSymbolBaseline: boolean, skipBaseline?: boolean): string | null { 777 let result = ""; 778 const gen = iterateBaseLine(isSymbolBaseline, skipBaseline); 779 for (let {done, value} = gen.next(); !done; { done, value } = gen.next()) { 780 const [, content] = value; 781 result += content; 782 } 783 return result || null; // eslint-disable-line no-null/no-null 784 } 785 786 function *iterateBaseLine(isSymbolBaseline: boolean, skipBaseline?: boolean): IterableIterator<[string, string]> { 787 if (skipBaseline) { 788 return; 789 } 790 const dupeCase = new ts.Map<string, number>(); 791 792 for (const file of allFiles) { 793 const { unitName } = file; 794 let typeLines = "=== " + unitName + " ===\r\n"; 795 const codeLines = ts.flatMap(file.content.split(/\r?\n/g), e => e.split(/[\r\u2028\u2029]/g)); 796 const gen: IterableIterator<TypeWriterResult> = isSymbolBaseline ? fullWalker.getSymbols(unitName) : fullWalker.getTypes(unitName); 797 let lastIndexWritten: number | undefined; 798 for (let {done, value: result} = gen.next(); !done; { done, value: result } = gen.next()) { 799 if (isSymbolBaseline && !result.symbol) { 800 return; 801 } 802 if (lastIndexWritten === undefined) { 803 typeLines += codeLines.slice(0, result.line + 1).join("\r\n") + "\r\n"; 804 } 805 else if (result.line !== lastIndexWritten) { 806 if (!((lastIndexWritten + 1 < codeLines.length) && (codeLines[lastIndexWritten + 1].match(/^\s*[{|}]\s*$/) || codeLines[lastIndexWritten + 1].trim() === ""))) { 807 typeLines += "\r\n"; 808 } 809 typeLines += codeLines.slice(lastIndexWritten + 1, result.line + 1).join("\r\n") + "\r\n"; 810 } 811 lastIndexWritten = result.line; 812 const typeOrSymbolString = isSymbolBaseline ? result.symbol : result.type; 813 const formattedLine = result.sourceText.replace(/\r?\n/g, "") + " : " + typeOrSymbolString; 814 typeLines += ">" + formattedLine + "\r\n"; 815 } 816 817 // Preserve legacy behavior 818 if (lastIndexWritten === undefined) { 819 for (const codeLine of codeLines) { 820 typeLines += codeLine + "\r\nNo type information for this code."; 821 } 822 } 823 else { 824 if (lastIndexWritten + 1 < codeLines.length) { 825 if (!((lastIndexWritten + 1 < codeLines.length) && (codeLines[lastIndexWritten + 1].match(/^\s*[{|}]\s*$/) || codeLines[lastIndexWritten + 1].trim() === ""))) { 826 typeLines += "\r\n"; 827 } 828 typeLines += codeLines.slice(lastIndexWritten + 1).join("\r\n"); 829 } 830 typeLines += "\r\n"; 831 } 832 yield [checkDuplicatedFileName(unitName, dupeCase), Utils.removeTestPathPrefixes(typeLines)]; 833 } 834 } 835 } 836 837 export function doSourcemapBaseline(baselinePath: string, options: ts.CompilerOptions, result: compiler.CompilationResult, harnessSettings: TestCaseParser.CompilerSettings) { 838 const declMaps = ts.getAreDeclarationMapsEnabled(options); 839 if (options.inlineSourceMap) { 840 if (result.maps.size > 0 && !declMaps) { 841 throw new Error("No sourcemap files should be generated if inlineSourceMaps was set."); 842 } 843 return; 844 } 845 else if (options.sourceMap || declMaps) { 846 if (result.maps.size !== ((options.sourceMap ? result.getNumberOfJsFiles(/*includeJson*/ false) : 0) + (declMaps ? result.getNumberOfJsFiles(/*includeJson*/ true) : 0))) { 847 throw new Error("Number of sourcemap files should be same as js files."); 848 } 849 850 let sourceMapCode: string | null; 851 if ((options.noEmitOnError && result.diagnostics.length !== 0) || result.maps.size === 0) { 852 // We need to return null here or the runBaseLine will actually create a empty file. 853 // Baselining isn't required here because there is no output. 854 sourceMapCode = null; // eslint-disable-line no-null/no-null 855 } 856 else { 857 sourceMapCode = ""; 858 result.maps.forEach(sourceMap => { 859 if (sourceMapCode) sourceMapCode += "\r\n"; 860 sourceMapCode += fileOutput(sourceMap, harnessSettings); 861 if (!options.inlineSourceMap) { 862 sourceMapCode += createSourceMapPreviewLink(sourceMap.text, result); 863 } 864 }); 865 } 866 Baseline.runBaseline(baselinePath.replace(/\.tsx?/, ".js.map"), sourceMapCode); 867 } 868 } 869 870 function createSourceMapPreviewLink(sourcemap: string, result: compiler.CompilationResult) { 871 const sourcemapJSON = JSON.parse(sourcemap); 872 const outputJSFile = result.outputs.find(td => td.file.endsWith(sourcemapJSON.file)); 873 if (!outputJSFile) return ""; 874 875 const sourceTDs = ts.map(sourcemapJSON.sources, (s: string) => result.inputs.find(td => td.file.endsWith(s))); 876 const anyUnfoundSources = ts.contains(sourceTDs, /*value*/ undefined); 877 if (anyUnfoundSources) return ""; 878 879 const hash = "#base64," + ts.map([outputJSFile.text, sourcemap].concat(sourceTDs.map(td => td!.text)), (s) => ts.convertToBase64(decodeURIComponent(encodeURIComponent(s)))).join(","); 880 return "\n//// https://sokra.github.io/source-map-visualization" + hash + "\n"; 881 } 882 883 export function doJsEmitBaseline(baselinePath: string, header: string, options: ts.CompilerOptions, result: compiler.CompilationResult, tsConfigFiles: readonly TestFile[], toBeCompiled: readonly TestFile[], otherFiles: readonly TestFile[], harnessSettings: TestCaseParser.CompilerSettings) { 884 if (!options.noEmit && !options.emitDeclarationOnly && result.js.size === 0 && result.diagnostics.length === 0) { 885 throw new Error("Expected at least one js file to be emitted or at least one error to be created."); 886 } 887 888 // check js output 889 let tsCode = ""; 890 const tsSources = otherFiles.concat(toBeCompiled); 891 if (tsSources.length > 1) { 892 tsCode += "//// [" + header + "] ////\r\n\r\n"; 893 } 894 for (let i = 0; i < tsSources.length; i++) { 895 tsCode += "//// [" + ts.getBaseFileName(tsSources[i].unitName) + "]\r\n"; 896 tsCode += tsSources[i].content + (i < (tsSources.length - 1) ? "\r\n" : ""); 897 } 898 899 let jsCode = ""; 900 result.js.forEach(file => { 901 if (jsCode.length && jsCode.charCodeAt(jsCode.length - 1) !== ts.CharacterCodes.lineFeed) { 902 jsCode += "\r\n"; 903 } 904 if (!result.diagnostics.length && !ts.endsWith(file.file, ts.Extension.Json)) { 905 const fileParseResult = ts.createSourceFile(file.file, file.text, options.target || ts.ScriptTarget.ES3, /*parentNodes*/ false, ts.endsWith(file.file, "x") ? ts.ScriptKind.JSX : ts.ScriptKind.JS); 906 if (ts.length(fileParseResult.parseDiagnostics)) { 907 jsCode += getErrorBaseline([file.asTestFile()], fileParseResult.parseDiagnostics); 908 return; 909 } 910 } 911 jsCode += fileOutput(file, harnessSettings); 912 }); 913 914 if (result.dts.size > 0) { 915 jsCode += "\r\n\r\n"; 916 result.dts.forEach(declFile => { 917 jsCode += fileOutput(declFile, harnessSettings); 918 }); 919 } 920 921 const declFileContext = prepareDeclarationCompilationContext( 922 toBeCompiled, otherFiles, result, harnessSettings, options, /*currentDirectory*/ undefined 923 ); 924 const declFileCompilationResult = compileDeclarationFiles(declFileContext, result.symlinks); 925 926 if (declFileCompilationResult && declFileCompilationResult.declResult.diagnostics.length) { 927 jsCode += "\r\n\r\n//// [DtsFileErrors]\r\n"; 928 jsCode += "\r\n\r\n"; 929 jsCode += getErrorBaseline(tsConfigFiles.concat(declFileCompilationResult.declInputFiles, declFileCompilationResult.declOtherFiles), declFileCompilationResult.declResult.diagnostics); 930 } 931 932 // eslint-disable-next-line no-null/no-null 933 Baseline.runBaseline(baselinePath.replace(/\.tsx?/, ts.Extension.Js), jsCode.length > 0 ? tsCode + "\r\n\r\n" + jsCode : null); 934 } 935 936 function fileOutput(file: documents.TextDocument, harnessSettings: TestCaseParser.CompilerSettings): string { 937 const fileName = harnessSettings.fullEmitPaths ? Utils.removeTestPathPrefixes(file.file) : ts.getBaseFileName(file.file); 938 return "//// [" + fileName + "]\r\n" + Utils.removeTestPathPrefixes(file.text); 939 } 940 941 export function collateOutputs(outputFiles: readonly documents.TextDocument[]): string { 942 const gen = iterateOutputs(outputFiles); 943 // Emit them 944 let result = ""; 945 for (let {done, value} = gen.next(); !done; { done, value } = gen.next()) { 946 // Some extra spacing if this isn't the first file 947 if (result.length) { 948 result += "\r\n\r\n"; 949 } 950 // FileName header + content 951 const [, content] = value; 952 result += content; 953 } 954 return result; 955 } 956 957 export function* iterateOutputs(outputFiles: Iterable<documents.TextDocument>): IterableIterator<[string, string]> { 958 // Collect, test, and sort the fileNames 959 const files = Array.from(outputFiles); 960 files.slice().sort((a, b) => ts.compareStringsCaseSensitive(cleanName(a.file), cleanName(b.file))); 961 const dupeCase = new ts.Map<string, number>(); 962 // Yield them 963 for (const outputFile of files) { 964 yield [checkDuplicatedFileName(outputFile.file, dupeCase), "/*====== " + outputFile.file + " ======*/\r\n" + Utils.removeByteOrderMark(outputFile.text)]; 965 } 966 967 function cleanName(fn: string) { 968 const lastSlash = ts.normalizeSlashes(fn).lastIndexOf("/"); 969 return fn.substr(lastSlash + 1).toLowerCase(); 970 } 971 } 972 973 function checkDuplicatedFileName(resultName: string, dupeCase: ts.ESMap<string, number>): string { 974 resultName = sanitizeTestFilePath(resultName); 975 if (dupeCase.has(resultName)) { 976 // A different baseline filename should be manufactured if the names differ only in case, for windows compat 977 const count = 1 + dupeCase.get(resultName)!; 978 dupeCase.set(resultName, count); 979 resultName = `${resultName}.dupe${count}`; 980 } 981 else { 982 dupeCase.set(resultName, 0); 983 } 984 return resultName; 985 } 986 987 export function sanitizeTestFilePath(name: string) { 988 const path = ts.toPath(ts.normalizeSlashes(name.replace(/[\^<>:"|?*%]/g, "_")).replace(/\.\.\//g, "__dotdot/"), "", Utils.canonicalizeForHarness); 989 if (ts.startsWith(path, "/")) { 990 return path.substring(1); 991 } 992 return path; 993 } 994 } 995 996 export interface FileBasedTest { 997 file: string; 998 configurations?: FileBasedTestConfiguration[]; 999 } 1000 1001 export interface FileBasedTestConfiguration { 1002 [key: string]: string; 1003 } 1004 1005 function splitVaryBySettingValue(text: string, varyBy: string): string[] | undefined { 1006 if (!text) return undefined; 1007 1008 let star = false; 1009 const includes: string[] = []; 1010 const excludes: string[] = []; 1011 for (let s of text.split(/,/g)) { 1012 s = s.trim().toLowerCase(); 1013 if (s.length === 0) continue; 1014 if (s === "*") { 1015 star = true; 1016 } 1017 else if (ts.startsWith(s, "-") || ts.startsWith(s, "!")) { 1018 excludes.push(s.slice(1)); 1019 } 1020 else { 1021 includes.push(s); 1022 } 1023 } 1024 1025 // do nothing if the setting has no variations 1026 if (includes.length <= 1 && !star && excludes.length === 0) { 1027 return undefined; 1028 } 1029 1030 const variations: { key: string, value?: string | number }[] = []; 1031 const values = getVaryByStarSettingValues(varyBy); 1032 1033 // add (and deduplicate) all included entries 1034 for (const include of includes) { 1035 const value = values?.get(include); 1036 if (ts.findIndex(variations, v => v.key === include || value !== undefined && v.value === value) === -1) { 1037 variations.push({ key: include, value }); 1038 } 1039 } 1040 1041 if (star && values) { 1042 // add all entries 1043 for (const [key, value] of ts.arrayFrom(values.entries())) { 1044 if (ts.findIndex(variations, v => v.key === key || v.value === value) === -1) { 1045 variations.push({ key, value }); 1046 } 1047 } 1048 } 1049 1050 // remove all excluded entries 1051 for (const exclude of excludes) { 1052 const value = values?.get(exclude); 1053 let index: number; 1054 while ((index = ts.findIndex(variations, v => v.key === exclude || value !== undefined && v.value === value)) >= 0) { 1055 ts.orderedRemoveItemAt(variations, index); 1056 } 1057 } 1058 1059 if (variations.length === 0) { 1060 throw new Error(`Variations in test option '@${varyBy}' resulted in an empty set.`); 1061 } 1062 1063 return ts.map(variations, v => v.key); 1064 } 1065 1066 function computeFileBasedTestConfigurationVariations(configurations: FileBasedTestConfiguration[], variationState: FileBasedTestConfiguration, varyByEntries: [string, string[]][], offset: number) { 1067 if (offset >= varyByEntries.length) { 1068 // make a copy of the current variation state 1069 configurations.push({ ...variationState }); 1070 return; 1071 } 1072 1073 const [varyBy, entries] = varyByEntries[offset]; 1074 for (const entry of entries) { 1075 // set or overwrite the variation, then compute the next variation 1076 variationState[varyBy] = entry; 1077 computeFileBasedTestConfigurationVariations(configurations, variationState, varyByEntries, offset + 1); 1078 } 1079 } 1080 1081 let booleanVaryByStarSettingValues: ts.ESMap<string, string | number> | undefined; 1082 1083 function getVaryByStarSettingValues(varyBy: string): ts.ReadonlyESMap<string, string | number> | undefined { 1084 const option = ts.forEach(ts.optionDeclarations, decl => ts.equateStringsCaseInsensitive(decl.name, varyBy) ? decl : undefined); 1085 if (option) { 1086 if (typeof option.type === "object") { 1087 return option.type; 1088 } 1089 if (option.type === "boolean") { 1090 return booleanVaryByStarSettingValues || (booleanVaryByStarSettingValues = new ts.Map(ts.getEntries({ 1091 true: 1, 1092 false: 0 1093 }))); 1094 } 1095 } 1096 } 1097 1098 /** 1099 * Compute FileBasedTestConfiguration variations based on a supplied list of variable settings. 1100 */ 1101 export function getFileBasedTestConfigurations(settings: TestCaseParser.CompilerSettings, varyBy: readonly string[]): FileBasedTestConfiguration[] | undefined { 1102 let varyByEntries: [string, string[]][] | undefined; 1103 let variationCount = 1; 1104 for (const varyByKey of varyBy) { 1105 if (ts.hasProperty(settings, varyByKey)) { 1106 // we only consider variations when there are 2 or more variable entries. 1107 const entries = splitVaryBySettingValue(settings[varyByKey], varyByKey); 1108 if (entries) { 1109 if (!varyByEntries) varyByEntries = []; 1110 variationCount *= entries.length; 1111 if (variationCount > 25) throw new Error(`Provided test options exceeded the maximum number of variations: ${varyBy.map(v => `'@${v}'`).join(", ")}`); 1112 varyByEntries.push([varyByKey, entries]); 1113 } 1114 } 1115 } 1116 1117 if (!varyByEntries) return undefined; 1118 1119 const configurations: FileBasedTestConfiguration[] = []; 1120 computeFileBasedTestConfigurationVariations(configurations, /*variationState*/ {}, varyByEntries, /*offset*/ 0); 1121 return configurations; 1122 } 1123 1124 /** 1125 * Compute a description for this configuration based on its entries 1126 */ 1127 export function getFileBasedTestConfigurationDescription(configuration: FileBasedTestConfiguration) { 1128 let name = ""; 1129 if (configuration) { 1130 const keys = Object.keys(configuration).sort(); 1131 for (const key of keys) { 1132 if (name) name += ", "; 1133 name += `@${key}: ${configuration[key]}`; 1134 } 1135 } 1136 return name; 1137 } 1138 1139 export namespace TestCaseParser { 1140 /** all the necessary information to set the right compiler settings */ 1141 export interface CompilerSettings { 1142 [name: string]: string; 1143 } 1144 1145 /** All the necessary information to turn a multi file test into useful units for later compilation */ 1146 export interface TestUnitData { 1147 content: string; 1148 name: string; 1149 fileOptions: any; 1150 originalFilePath: string; 1151 references: string[]; 1152 } 1153 1154 // Regex for parsing options in the format "@Alpha: Value of any sort" 1155 const optionRegex = /^[\/]{2}\s*@(\w+)\s*:\s*([^\r\n]*)/gm; // multiple matches on multiple lines 1156 const linkRegex = /^[\/]{2}\s*@link\s*:\s*([^\r\n]*)\s*->\s*([^\r\n]*)/gm; // multiple matches on multiple lines 1157 1158 export function parseSymlinkFromTest(line: string, symlinks: vfs.FileSet | undefined) { 1159 const linkMetaData = linkRegex.exec(line); 1160 linkRegex.lastIndex = 0; 1161 if (!linkMetaData) return undefined; 1162 1163 if (!symlinks) symlinks = {}; 1164 symlinks[linkMetaData[2].trim()] = new vfs.Symlink(linkMetaData[1].trim()); 1165 return symlinks; 1166 } 1167 1168 export function extractCompilerSettings(content: string): CompilerSettings { 1169 const opts: CompilerSettings = {}; 1170 1171 let match: RegExpExecArray | null; 1172 while ((match = optionRegex.exec(content)) !== null) { // eslint-disable-line no-null/no-null 1173 opts[match[1]] = match[2].trim(); 1174 } 1175 1176 return opts; 1177 } 1178 1179 export interface TestCaseContent { 1180 settings: CompilerSettings; 1181 testUnitData: TestUnitData[]; 1182 tsConfig: ts.ParsedCommandLine | undefined; 1183 tsConfigFileUnitData: TestUnitData | undefined; 1184 symlinks?: vfs.FileSet; 1185 } 1186 1187 /** Given a test file containing // @FileName directives, return an array of named units of code to be added to an existing compiler instance */ 1188 export function makeUnitsFromTest(code: string, fileName: string, rootDir?: string, settings = extractCompilerSettings(code)): TestCaseContent { 1189 // List of all the subfiles we've parsed out 1190 const testUnitData: TestUnitData[] = []; 1191 1192 const lines = Utils.splitContentByNewlines(code); 1193 1194 // Stuff related to the subfile we're parsing 1195 let currentFileContent: string | undefined; 1196 let currentFileOptions: any = {}; 1197 let currentFileName: any; 1198 let refs: string[] = []; 1199 let symlinks: vfs.FileSet | undefined; 1200 1201 for (const line of lines) { 1202 let testMetaData: RegExpExecArray | null; 1203 const possiblySymlinks = parseSymlinkFromTest(line, symlinks); 1204 if (possiblySymlinks) { 1205 symlinks = possiblySymlinks; 1206 } 1207 else if (testMetaData = optionRegex.exec(line)) { 1208 // Comment line, check for global/file @options and record them 1209 optionRegex.lastIndex = 0; 1210 const metaDataName = testMetaData[1].toLowerCase(); 1211 currentFileOptions[testMetaData[1]] = testMetaData[2].trim(); 1212 if (metaDataName !== "filename") { 1213 continue; 1214 } 1215 1216 // New metadata statement after having collected some code to go with the previous metadata 1217 if (currentFileName) { 1218 // Store result file 1219 const newTestFile = { 1220 content: currentFileContent!, // TODO: GH#18217 1221 name: currentFileName, 1222 fileOptions: currentFileOptions, 1223 originalFilePath: fileName, 1224 references: refs 1225 }; 1226 testUnitData.push(newTestFile); 1227 1228 // Reset local data 1229 currentFileContent = undefined; 1230 currentFileOptions = {}; 1231 currentFileName = testMetaData[2].trim(); 1232 refs = []; 1233 } 1234 else { 1235 // First metadata marker in the file 1236 currentFileName = testMetaData[2].trim(); 1237 } 1238 } 1239 else { 1240 // Subfile content line 1241 // Append to the current subfile content, inserting a newline needed 1242 if (currentFileContent === undefined) { 1243 currentFileContent = ""; 1244 } 1245 else if (currentFileContent !== "") { 1246 // End-of-line 1247 currentFileContent = currentFileContent + "\n"; 1248 } 1249 currentFileContent = currentFileContent + line; 1250 } 1251 } 1252 1253 // normalize the fileName for the single file case 1254 currentFileName = testUnitData.length > 0 || currentFileName ? currentFileName : ts.getBaseFileName(fileName); 1255 1256 // EOF, push whatever remains 1257 const newTestFile2 = { 1258 content: currentFileContent || "", 1259 name: currentFileName, 1260 fileOptions: currentFileOptions, 1261 originalFilePath: fileName, 1262 references: refs 1263 }; 1264 testUnitData.push(newTestFile2); 1265 1266 // unit tests always list files explicitly 1267 const parseConfigHost: ts.ParseConfigHost = { 1268 useCaseSensitiveFileNames: false, 1269 readDirectory: () => [], 1270 fileExists: () => true, 1271 readFile: (name) => ts.forEach(testUnitData, data => data.name.toLowerCase() === name.toLowerCase() ? data.content : undefined) 1272 }; 1273 1274 // check if project has tsconfig.json in the list of files 1275 let tsConfig: ts.ParsedCommandLine | undefined; 1276 let tsConfigFileUnitData: TestUnitData | undefined; 1277 for (let i = 0; i < testUnitData.length; i++) { 1278 const data = testUnitData[i]; 1279 if (getConfigNameFromFileName(data.name)) { 1280 const configJson = ts.parseJsonText(data.name, data.content); 1281 assert.isTrue(configJson.endOfFileToken !== undefined); 1282 let baseDir = ts.normalizePath(ts.getDirectoryPath(data.name)); 1283 if (rootDir) { 1284 baseDir = ts.getNormalizedAbsolutePath(baseDir, rootDir); 1285 } 1286 tsConfig = ts.parseJsonSourceFileConfigFileContent(configJson, parseConfigHost, baseDir); 1287 tsConfig.options.configFilePath = data.name; 1288 tsConfigFileUnitData = data; 1289 1290 // delete entry from the list 1291 ts.orderedRemoveItemAt(testUnitData, i); 1292 1293 break; 1294 } 1295 } 1296 return { settings, testUnitData, tsConfig, tsConfigFileUnitData, symlinks }; 1297 } 1298 } 1299 1300 /** Support class for baseline files */ 1301 export namespace Baseline { 1302 const noContent = "<no content>"; 1303 1304 export interface BaselineOptions { 1305 Subfolder?: string; 1306 Baselinefolder?: string; 1307 PrintDiff?: true; 1308 } 1309 1310 export function localPath(fileName: string, baselineFolder?: string, subfolder?: string) { 1311 if (baselineFolder === undefined) { 1312 return baselinePath(fileName, "local", "tests/baselines", subfolder); 1313 } 1314 else { 1315 return baselinePath(fileName, "local", baselineFolder, subfolder); 1316 } 1317 } 1318 1319 function referencePath(fileName: string, baselineFolder?: string, subfolder?: string) { 1320 if (baselineFolder === undefined) { 1321 return baselinePath(fileName, "reference", "tests/baselines", subfolder); 1322 } 1323 else { 1324 return baselinePath(fileName, "reference", baselineFolder, subfolder); 1325 } 1326 } 1327 1328 function baselinePath(fileName: string, type: string, baselineFolder: string, subfolder?: string) { 1329 if (subfolder !== undefined) { 1330 return userSpecifiedRoot + baselineFolder + "/" + subfolder + "/" + type + "/" + fileName; 1331 } 1332 else { 1333 return userSpecifiedRoot + baselineFolder + "/" + type + "/" + fileName; 1334 } 1335 } 1336 1337 const fileCache: { [idx: string]: boolean } = {}; 1338 1339 function compareToBaseline(actual: string | null, relativeFileName: string, opts: BaselineOptions | undefined) { 1340 // actual is now either undefined (the generator had an error), null (no file requested), 1341 // or some real output of the function 1342 if (actual === undefined) { 1343 // Nothing to do 1344 return undefined!; // TODO: GH#18217 1345 } 1346 1347 const refFileName = referencePath(relativeFileName, opts && opts.Baselinefolder, opts && opts.Subfolder); 1348 1349 // eslint-disable-next-line no-null/no-null 1350 if (actual === null) { 1351 actual = noContent; 1352 } 1353 1354 let expected = "<no content>"; 1355 if (IO.fileExists(refFileName)) { 1356 expected = IO.readFile(refFileName)!; // TODO: GH#18217 1357 } 1358 1359 return { expected, actual }; 1360 } 1361 1362 function writeComparison(expected: string, actual: string, relativeFileName: string, actualFileName: string, opts?: BaselineOptions) { 1363 // For now this is written using TypeScript, because sys is not available when running old test cases. 1364 // But we need to move to sys once we have 1365 // Creates the directory including its parent if not already present 1366 function createDirectoryStructure(dirName: string) { 1367 if (fileCache[dirName] || IO.directoryExists(dirName)) { 1368 fileCache[dirName] = true; 1369 return; 1370 } 1371 1372 const parentDirectory = IO.directoryName(dirName)!; // TODO: GH#18217 1373 if (parentDirectory !== "" && parentDirectory !== dirName) { 1374 createDirectoryStructure(parentDirectory); 1375 } 1376 IO.createDirectory(dirName); 1377 fileCache[dirName] = true; 1378 } 1379 1380 // Create folders if needed 1381 createDirectoryStructure(IO.directoryName(actualFileName)!); // TODO: GH#18217 1382 1383 // Delete the actual file in case it fails 1384 if (IO.fileExists(actualFileName)) { 1385 IO.deleteFile(actualFileName); 1386 } 1387 1388 const encodedActual = Utils.encodeString(actual); 1389 if (expected !== encodedActual) { 1390 if (actual === noContent) { 1391 IO.writeFile(actualFileName + ".delete", ""); 1392 } 1393 else { 1394 IO.writeFile(actualFileName, encodedActual); 1395 } 1396 if (!!require && opts && opts.PrintDiff) { 1397 const Diff = require("diff"); 1398 const patch = Diff.createTwoFilesPatch("Expected", "Actual", expected, actual, "The current baseline", "The new version"); 1399 throw new Error(`The baseline file ${relativeFileName} has changed.${ts.ForegroundColorEscapeSequences.Grey}\n\n${patch}`); 1400 } 1401 else { 1402 throw new Error(`The baseline file ${relativeFileName} has changed.`); 1403 } 1404 } 1405 } 1406 1407 export function runBaseline(relativeFileName: string, actual: string | null, opts?: BaselineOptions): void { 1408 const actualFileName = localPath(relativeFileName, opts && opts.Baselinefolder, opts && opts.Subfolder); 1409 if (actual === undefined) { 1410 throw new Error("The generated content was \"undefined\". Return \"null\" if no baselining is required.\""); 1411 } 1412 const comparison = compareToBaseline(actual, relativeFileName, opts); 1413 writeComparison(comparison.expected, comparison.actual, relativeFileName, actualFileName, opts); 1414 } 1415 1416 export function runMultifileBaseline(relativeFileBase: string, extension: string, generateContent: () => IterableIterator<[string, string, number]> | IterableIterator<[string, string]> | null, opts?: BaselineOptions, referencedExtensions?: string[]): void { 1417 const gen = generateContent(); 1418 const writtenFiles = new ts.Map<string, true>(); 1419 const errors: Error[] = []; 1420 1421 // eslint-disable-next-line no-null/no-null 1422 if (gen !== null) { 1423 for (let {done, value} = gen.next(); !done; { done, value } = gen.next()) { 1424 const [name, content, count] = value as [string, string, number | undefined]; 1425 if (count === 0) continue; // Allow error reporter to skip writing files without errors 1426 const relativeFileName = relativeFileBase + "/" + name + extension; 1427 const actualFileName = localPath(relativeFileName, opts && opts.Baselinefolder, opts && opts.Subfolder); 1428 const comparison = compareToBaseline(content, relativeFileName, opts); 1429 try { 1430 writeComparison(comparison.expected, comparison.actual, relativeFileName, actualFileName); 1431 } 1432 catch (e) { 1433 errors.push(e); 1434 } 1435 writtenFiles.set(relativeFileName, true); 1436 } 1437 } 1438 1439 const referenceDir = referencePath(relativeFileBase, opts && opts.Baselinefolder, opts && opts.Subfolder); 1440 let existing = IO.readDirectory(referenceDir, referencedExtensions || [extension]); 1441 if (extension === ".ts" || referencedExtensions && referencedExtensions.indexOf(".ts") > -1 && referencedExtensions.indexOf(".d.ts") === -1) { 1442 // special-case and filter .d.ts out of .ts results 1443 existing = existing.filter(f => !ts.endsWith(f, ".d.ts")); 1444 } 1445 const missing: string[] = []; 1446 for (const name of existing) { 1447 const localCopy = name.substring(referenceDir.length - relativeFileBase.length); 1448 if (!writtenFiles.has(localCopy)) { 1449 missing.push(localCopy); 1450 } 1451 } 1452 if (missing.length) { 1453 for (const file of missing) { 1454 IO.writeFile(localPath(file + ".delete", opts && opts.Baselinefolder, opts && opts.Subfolder), ""); 1455 } 1456 } 1457 1458 if (errors.length || missing.length) { 1459 let errorMsg = ""; 1460 if (errors.length) { 1461 errorMsg += `The baseline for ${relativeFileBase} in ${errors.length} files has changed:${"\n " + errors.slice(0, 5).map(e => e.message).join("\n ") + (errors.length > 5 ? "\n" + ` and ${errors.length - 5} more` : "")}`; 1462 } 1463 if (errors.length && missing.length) { 1464 errorMsg += "\n"; 1465 } 1466 if (missing.length) { 1467 const writtenFilesArray = ts.arrayFrom(writtenFiles.keys()); 1468 errorMsg += `Baseline missing ${missing.length} files:${"\n " + missing.slice(0, 5).join("\n ") + (missing.length > 5 ? "\n" + ` and ${missing.length - 5} more` : "") + "\n"}Written ${writtenFiles.size} files:${"\n " + writtenFilesArray.slice(0, 5).join("\n ") + (writtenFilesArray.length > 5 ? "\n" + ` and ${writtenFilesArray.length - 5} more` : "")}`; 1469 } 1470 throw new Error(errorMsg); 1471 } 1472 } 1473 } 1474 1475 export function isDefaultLibraryFile(filePath: string): boolean { 1476 // We need to make sure that the filePath is prefixed with "lib." not just containing "lib." and end with ".d.ts" 1477 const fileName = ts.getBaseFileName(ts.normalizeSlashes(filePath)); 1478 return ts.startsWith(fileName, "lib.") && (ts.endsWith(fileName, ts.Extension.Dts) || ts.endsWith(fileName, ts.Extension.Dets)); 1479 } 1480 1481 export function isBuiltFile(filePath: string): boolean { 1482 return filePath.indexOf(libFolder) === 0 || 1483 filePath.indexOf(vpath.addTrailingSeparator(vfs.builtFolder)) === 0; 1484 } 1485 1486 export function getDefaultLibraryFile(filePath: string, io: IO): Compiler.TestFile { 1487 const libFile = userSpecifiedRoot + libFolder + ts.getBaseFileName(ts.normalizeSlashes(filePath)); 1488 return { unitName: libFile, content: io.readFile(libFile)! }; 1489 } 1490 1491 export function getConfigNameFromFileName(filename: string): "tsconfig.json" | "jsconfig.json" | undefined { 1492 const flc = ts.getBaseFileName(filename).toLowerCase(); 1493 return ts.find(["tsconfig.json" as "tsconfig.json", "jsconfig.json" as "jsconfig.json"], x => x === flc); 1494 } 1495 1496 if (Error) (<any>Error).stackTraceLimit = 100; 1497} 1498