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