• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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