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