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