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