• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import * as ts from "../../_namespaces/ts";
2import * as fakes from "../../_namespaces/fakes";
3import * as vfs from "../../_namespaces/vfs";
4import * as Harness from "../../_namespaces/Harness";
5import * as vpath from "../../_namespaces/vpath";
6
7export function errorDiagnostic(message: fakes.ExpectedDiagnosticMessage): fakes.ExpectedErrorDiagnostic {
8    return { message };
9}
10
11export function getExpectedDiagnosticForProjectsInBuild(...projects: string[]): fakes.ExpectedDiagnostic {
12    return [ts.Diagnostics.Projects_in_this_build_Colon_0, projects.map(p => "\r\n    * " + p).join("")];
13}
14
15export function changeCompilerVersion(host: fakes.SolutionBuilderHost) {
16    const originalReadFile = host.readFile;
17    host.readFile = path => {
18        const value = originalReadFile.call(host, path);
19        if (!value || !ts.isBuildInfoFile(path)) return value;
20        const buildInfo = ts.getBuildInfo(path, value);
21        if (!buildInfo) return value;
22        buildInfo.version = fakes.version;
23        return ts.getBuildInfoText(buildInfo);
24    };
25}
26
27export function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) {
28    if (!fs.statSync(path).isFile()) {
29        throw new Error(`File ${path} does not exist`);
30    }
31    const old = fs.readFileSync(path, "utf-8");
32    if (old.indexOf(oldText) < 0) {
33        throw new Error(`Text "${oldText}" does not exist in file ${path}`);
34    }
35    const newContent = old.replace(oldText, newText);
36    fs.writeFileSync(path, newContent, "utf-8");
37}
38
39export function prependText(fs: vfs.FileSystem, path: string, additionalContent: string) {
40    if (!fs.statSync(path).isFile()) {
41        throw new Error(`File ${path} does not exist`);
42    }
43    const old = fs.readFileSync(path, "utf-8");
44    fs.writeFileSync(path, `${additionalContent}${old}`, "utf-8");
45}
46
47export function appendText(fs: vfs.FileSystem, path: string, additionalContent: string) {
48    if (!fs.statSync(path).isFile()) {
49        throw new Error(`File ${path} does not exist`);
50    }
51    const old = fs.readFileSync(path, "utf-8");
52    fs.writeFileSync(path, `${old}${additionalContent}`);
53}
54
55export function indexOf(fs: vfs.FileSystem, path: string, searchStr: string) {
56    if (!fs.statSync(path).isFile()) {
57        throw new Error(`File ${path} does not exist`);
58    }
59    const content = fs.readFileSync(path, "utf-8");
60    return content.indexOf(searchStr);
61}
62
63export function lastIndexOf(fs: vfs.FileSystem, path: string, searchStr: string) {
64    if (!fs.statSync(path).isFile()) {
65        throw new Error(`File ${path} does not exist`);
66    }
67    const content = fs.readFileSync(path, "utf-8");
68    return content.lastIndexOf(searchStr);
69}
70
71export function expectedLocationIndexOf(fs: vfs.FileSystem, file: string, searchStr: string): fakes.ExpectedDiagnosticLocation {
72    return {
73        file,
74        start: indexOf(fs, file, searchStr),
75        length: searchStr.length
76    };
77}
78
79export function expectedLocationLastIndexOf(fs: vfs.FileSystem, file: string, searchStr: string): fakes.ExpectedDiagnosticLocation {
80    return {
81        file,
82        start: lastIndexOf(fs, file, searchStr),
83        length: searchStr.length
84    };
85}
86
87export const libContent = `${ts.TestFSWithWatch.libFile.content}
88interface ReadonlyArray<T> {}
89declare const console: { log(msg: any): void; };`;
90
91export const symbolLibContent = `
92interface SymbolConstructor {
93    readonly species: symbol;
94    readonly toStringTag: symbol;
95}
96declare var Symbol: SymbolConstructor;
97interface Symbol {
98    readonly [Symbol.toStringTag]: string;
99}
100`;
101
102/**
103 * Load project from disk into /src folder
104 */
105export function loadProjectFromDisk(
106    root: string,
107    libContentToAppend?: string
108): vfs.FileSystem {
109    const resolver = vfs.createResolver(Harness.IO);
110    const fs = new vfs.FileSystem(/*ignoreCase*/ true, {
111        files: {
112            ["/src"]: new vfs.Mount(vpath.resolve(Harness.IO.getWorkspaceRoot(), root), resolver)
113        },
114        cwd: "/",
115        meta: { defaultLibLocation: "/lib" },
116    });
117    addLibAndMakeReadonly(fs, libContentToAppend);
118    return fs;
119}
120
121/**
122 * All the files must be in /src
123 */
124export function loadProjectFromFiles(
125    files: vfs.FileSet,
126    libContentToAppend?: string
127): vfs.FileSystem {
128    const fs = new vfs.FileSystem(/*ignoreCase*/ true, {
129        files,
130        cwd: "/",
131        meta: { defaultLibLocation: "/lib" },
132    });
133    addLibAndMakeReadonly(fs, libContentToAppend);
134    return fs;
135}
136
137function addLibAndMakeReadonly(fs: vfs.FileSystem, libContentToAppend?: string) {
138    fs.mkdirSync("/lib");
139    fs.writeFileSync("/lib/lib.d.ts", libContentToAppend ? `${libContent}${libContentToAppend}` : libContent);
140    fs.makeReadonly();
141}
142
143export function verifyOutputsPresent(fs: vfs.FileSystem, outputs: readonly string[]) {
144    for (const output of outputs) {
145        assert(fs.existsSync(output), `Expect file ${output} to exist`);
146    }
147}
148
149export function verifyOutputsAbsent(fs: vfs.FileSystem, outputs: readonly string[]) {
150    for (const output of outputs) {
151        assert.isFalse(fs.existsSync(output), `Expect file ${output} to not exist`);
152    }
153}
154
155export function generateSourceMapBaselineFiles(sys: ts.System & { writtenFiles: ts.ReadonlyCollection<ts.Path>; }) {
156    const mapFileNames = ts.mapDefinedIterator(sys.writtenFiles.keys(), f => f.endsWith(".map") ? f : undefined);
157    while (true) {
158        const result = mapFileNames.next();
159        if (result.done) break;
160        const mapFile = result.value;
161        const text = Harness.SourceMapRecorder.getSourceMapRecordWithSystem(sys, mapFile);
162        sys.writeFile(`${mapFile}.baseline.txt`, text);
163    }
164}
165
166function generateBundleFileSectionInfo(sys: ts.System, originalReadCall: ts.System["readFile"], baselineRecorder: Harness.Compiler.WriterAggregator, bundleFileInfo: ts.BundleFileInfo | undefined, outFile: string | undefined) {
167    if (!ts.length(bundleFileInfo && bundleFileInfo.sections) && !outFile) return; // Nothing to baseline
168
169    const content = outFile && sys.fileExists(outFile) ? originalReadCall.call(sys, outFile, "utf8")! : "";
170    baselineRecorder.WriteLine("======================================================================");
171    baselineRecorder.WriteLine(`File:: ${outFile}`);
172    for (const section of bundleFileInfo ? bundleFileInfo.sections : ts.emptyArray) {
173        baselineRecorder.WriteLine("----------------------------------------------------------------------");
174        writeSectionHeader(section);
175        if (section.kind !== ts.BundleFileSectionKind.Prepend) {
176            writeTextOfSection(section.pos, section.end);
177        }
178        else if (section.texts.length > 0) {
179            ts.Debug.assert(section.pos === ts.first(section.texts).pos);
180            ts.Debug.assert(section.end === ts.last(section.texts).end);
181            for (const text of section.texts) {
182                baselineRecorder.WriteLine(">>--------------------------------------------------------------------");
183                writeSectionHeader(text);
184                writeTextOfSection(text.pos, text.end);
185            }
186        }
187        else {
188            ts.Debug.assert(section.pos === section.end);
189        }
190    }
191    baselineRecorder.WriteLine("======================================================================");
192
193    function writeTextOfSection(pos: number, end: number) {
194        const textLines = content.substring(pos, end).split(/\r?\n/);
195        for (const line of textLines) {
196            baselineRecorder.WriteLine(line);
197        }
198    }
199
200    function writeSectionHeader(section: ts.BundleFileSection) {
201        baselineRecorder.WriteLine(`${section.kind}: (${section.pos}-${section.end})${section.data ? ":: " + section.data : ""}${section.kind === ts.BundleFileSectionKind.Prepend ? " texts:: " + section.texts.length : ""}`);
202    }
203}
204
205type ReadableProgramBuildInfoDiagnostic = string | [string, readonly ts.ReusableDiagnostic[]];
206type ReadableProgramBuilderInfoFilePendingEmit = [string, "DtsOnly" | "Full"];
207type ReadableProgramBuildInfoEmitSignature = string | [string, string];
208type ReadableProgramBuildInfoFileInfo = Omit<ts.BuilderState.FileInfo, "impliedFormat"> & { impliedFormat: string | undefined; };
209type ReadableProgramMultiFileEmitBuildInfo = Omit<ts.ProgramMultiFileEmitBuildInfo,
210    "fileIdsList" | "fileInfos" |
211    "referencedMap" | "exportedModulesMap" | "semanticDiagnosticsPerFile" |
212    "affectedFilesPendingEmit" | "changeFileSet" | "emitSignatures"
213> & {
214    fileNamesList: readonly (readonly string[])[] | undefined;
215    fileInfos: ts.MapLike<ReadableProgramBuildInfoFileInfo>;
216    referencedMap?: ts.MapLike<string[]>;
217    exportedModulesMap?: ts.MapLike<string[]>;
218    semanticDiagnosticsPerFile?: readonly ReadableProgramBuildInfoDiagnostic[];
219    affectedFilesPendingEmit?: readonly ReadableProgramBuilderInfoFilePendingEmit[];
220    changeFileSet?: readonly string[];
221    emitSignatures?: readonly ReadableProgramBuildInfoEmitSignature[];
222};
223type ReadableProgramBundleEmitBuildInfo = Omit<ts.ProgramBundleEmitBuildInfo, "fileInfos"> & {
224    fileInfos: ts.MapLike<string>;
225};
226
227type ReadableProgramBuildInfo = ReadableProgramMultiFileEmitBuildInfo | ReadableProgramBundleEmitBuildInfo;
228
229function isReadableProgramBundleEmitBuildInfo(info: ReadableProgramBuildInfo | undefined): info is ReadableProgramBundleEmitBuildInfo {
230    return !!info && !!ts.outFile(info.options || {});
231}
232type ReadableBuildInfo = Omit<ts.BuildInfo, "program"> & { program: ReadableProgramBuildInfo | undefined; size: number; };
233function generateBuildInfoProgramBaseline(sys: ts.System, buildInfoPath: string, buildInfo: ts.BuildInfo) {
234    let program: ReadableProgramBuildInfo | undefined;
235    let fileNamesList: string[][] | undefined;
236    if (buildInfo.program && ts.isProgramBundleEmitBuildInfo(buildInfo.program)) {
237        const fileInfos: ReadableProgramBundleEmitBuildInfo["fileInfos"] = {};
238        buildInfo.program?.fileInfos?.forEach((fileInfo, index) => fileInfos[toFileName(index + 1 as ts.ProgramBuildInfoFileId)] = fileInfo);
239        program = {
240            ...buildInfo.program,
241            fileInfos
242        };
243    }
244    else if (buildInfo.program) {
245        const fileInfos: ReadableProgramMultiFileEmitBuildInfo["fileInfos"] = {};
246        buildInfo.program?.fileInfos?.forEach((fileInfo, index) => fileInfos[toFileName(index + 1 as ts.ProgramBuildInfoFileId)] = toReadableFileInfo(fileInfo));
247        fileNamesList = buildInfo.program.fileIdsList?.map(fileIdsListId => fileIdsListId.map(toFileName));
248        program = buildInfo.program && {
249            fileNames: buildInfo.program.fileNames,
250            fileNamesList,
251            fileInfos: buildInfo.program.fileInfos ? fileInfos : undefined!,
252            options: buildInfo.program.options,
253            referencedMap: toMapOfReferencedSet(buildInfo.program.referencedMap),
254            exportedModulesMap: toMapOfReferencedSet(buildInfo.program.exportedModulesMap),
255            semanticDiagnosticsPerFile: buildInfo.program.semanticDiagnosticsPerFile?.map(d =>
256                ts.isNumber(d) ?
257                    toFileName(d) :
258                    [toFileName(d[0]), d[1]]
259            ),
260            affectedFilesPendingEmit: buildInfo.program.affectedFilesPendingEmit?.map(([fileId, emitKind]) => [
261                toFileName(fileId),
262                emitKind === ts.BuilderFileEmit.DtsOnly ? "DtsOnly" :
263                    emitKind === ts.BuilderFileEmit.Full ? "Full" :
264                        ts.Debug.assertNever(emitKind)
265            ]),
266            changeFileSet: buildInfo.program.changeFileSet?.map(toFileName),
267            emitSignatures: buildInfo.program.emitSignatures?.map(s =>
268                ts.isNumber(s) ?
269                    toFileName(s) :
270                    [toFileName(s[0]), s[1]]
271            ),
272            latestChangedDtsFile: buildInfo.program.latestChangedDtsFile,
273        };
274    }
275    const version = buildInfo.version === ts.version ? fakes.version : buildInfo.version;
276    const result: ReadableBuildInfo = {
277        // Baseline fixed order for bundle
278        bundle: buildInfo.bundle && {
279            ...buildInfo.bundle,
280            js: buildInfo.bundle.js && {
281                sections: buildInfo.bundle.js.sections,
282                hash: buildInfo.bundle.js.hash,
283                mapHash: buildInfo.bundle.js.mapHash,
284                sources: buildInfo.bundle.js.sources,
285            },
286            dts: buildInfo.bundle.dts && {
287                sections: buildInfo.bundle.dts.sections,
288                hash: buildInfo.bundle.dts.hash,
289                mapHash: buildInfo.bundle.dts.mapHash,
290                sources: buildInfo.bundle.dts.sources,
291            },
292        },
293        program,
294        version,
295        size: ts.getBuildInfoText({ ...buildInfo, version }).length,
296    };
297    // For now its just JSON.stringify
298    sys.writeFile(`${buildInfoPath}.readable.baseline.txt`, JSON.stringify(result, /*replacer*/ undefined, 2));
299
300    function toFileName(fileId: ts.ProgramBuildInfoFileId) {
301        return buildInfo.program!.fileNames[fileId - 1];
302    }
303
304    function toFileNames(fileIdsListId: ts.ProgramBuildInfoFileIdListId) {
305        return fileNamesList![fileIdsListId - 1];
306    }
307
308    function toReadableFileInfo(fileInfo: ts.ProgramBuildInfoFileInfo): ReadableProgramBuildInfoFileInfo {
309        const info = ts.toBuilderStateFileInfo(fileInfo);
310        return {
311            ...info,
312            impliedFormat: info.impliedFormat && ts.getNameOfCompilerOptionValue(info.impliedFormat, ts.moduleOptionDeclaration.type),
313        };
314    }
315
316    function toMapOfReferencedSet(referenceMap: ts.ProgramBuildInfoReferencedMap | undefined): ts.MapLike<string[]> | undefined {
317        if (!referenceMap) return undefined;
318        const result: ts.MapLike<string[]> = {};
319        for (const [fileNamesKey, fileNamesListKey] of referenceMap) {
320            result[toFileName(fileNamesKey)] = toFileNames(fileNamesListKey);
321        }
322        return result;
323    }
324}
325
326export function toPathWithSystem(sys: ts.System, fileName: string): ts.Path {
327    return ts.toPath(fileName, sys.getCurrentDirectory(), ts.createGetCanonicalFileName(sys.useCaseSensitiveFileNames));
328}
329
330export function baselineBuildInfo(
331    options: ts.CompilerOptions,
332    sys: ts.TscCompileSystem | ts.tscWatch.WatchedSystem,
333    originalReadCall?: ts.System["readFile"],
334) {
335    const buildInfoPath = ts.getTsBuildInfoEmitOutputFilePath(options);
336    if (!buildInfoPath || !sys.writtenFiles!.has(toPathWithSystem(sys, buildInfoPath))) return;
337    if (!sys.fileExists(buildInfoPath)) return;
338
339    const buildInfo = ts.getBuildInfo(buildInfoPath, (originalReadCall || sys.readFile).call(sys, buildInfoPath, "utf8")!);
340    if (!buildInfo) return sys.writeFile(`${buildInfoPath}.baseline.txt`, "Error reading valid buildinfo file");
341    generateBuildInfoProgramBaseline(sys, buildInfoPath, buildInfo);
342
343    if (!ts.outFile(options)) return;
344    const { jsFilePath, declarationFilePath } = ts.getOutputPathsForBundle(options, /*forceDts*/ false);
345    const bundle = buildInfo.bundle;
346    if (!bundle || (!ts.length(bundle.js && bundle.js.sections) && !ts.length(bundle.dts && bundle.dts.sections))) return;
347
348    // Write the baselines:
349    const baselineRecorder = new Harness.Compiler.WriterAggregator();
350    generateBundleFileSectionInfo(sys, originalReadCall || sys.readFile, baselineRecorder, bundle.js, jsFilePath);
351    generateBundleFileSectionInfo(sys, originalReadCall || sys.readFile, baselineRecorder, bundle.dts, declarationFilePath);
352    baselineRecorder.Close();
353    const text = baselineRecorder.lines.join("\r\n");
354    sys.writeFile(`${buildInfoPath}.baseline.txt`, text);
355}
356interface VerifyTscEditDiscrepanciesInput {
357    index: number;
358    scenario: ts.TestTscCompile["scenario"];
359    subScenario: ts.TestTscCompile["subScenario"];
360    baselines: string[] | undefined;
361    commandLineArgs: ts.TestTscCompile["commandLineArgs"];
362    modifyFs: ts.TestTscCompile["modifyFs"];
363    editFs: TestTscEdit["modifyFs"];
364    baseFs: vfs.FileSystem;
365    newSys: ts.TscCompileSystem;
366    discrepancyExplanation: TestTscEdit["discrepancyExplanation"];
367}
368function verifyTscEditDiscrepancies({
369    index, scenario, subScenario, commandLineArgs,
370    discrepancyExplanation, baselines,
371    modifyFs, editFs, baseFs, newSys
372}: VerifyTscEditDiscrepanciesInput): string[] | undefined {
373    const sys = ts.testTscCompile({
374        scenario,
375        subScenario,
376        fs: () => baseFs.makeReadonly(),
377        commandLineArgs,
378        modifyFs: fs => {
379            if (modifyFs) modifyFs(fs);
380            editFs(fs);
381        },
382        disableUseFileVersionAsSignature: true,
383    });
384    let headerAdded = false;
385    for (const outputFile of ts.arrayFrom(sys.writtenFiles.keys())) {
386        const cleanBuildText = sys.readFile(outputFile);
387        const incrementalBuildText = newSys.readFile(outputFile);
388        if (ts.isBuildInfoFile(outputFile)) {
389            // Check only presence and absence and not text as we will do that for readable baseline
390            if (!sys.fileExists(`${outputFile}.readable.baseline.txt`)) addBaseline(`Readable baseline not present in clean build:: File:: ${outputFile}`);
391            if (!newSys.fileExists(`${outputFile}.readable.baseline.txt`)) addBaseline(`Readable baseline not present in incremental build:: File:: ${outputFile}`);
392            verifyPresenceAbsence(incrementalBuildText, cleanBuildText, `Incremental and clean tsbuildinfo file presence differs:: File:: ${outputFile}`);
393        }
394        else if (!ts.fileExtensionIs(outputFile, ".tsbuildinfo.readable.baseline.txt")) {
395            verifyTextEqual(incrementalBuildText, cleanBuildText, `File: ${outputFile}`);
396        }
397        else if (incrementalBuildText !== cleanBuildText) {
398            // Verify build info without affectedFilesPendingEmit
399            const { buildInfo: incrementalBuildInfo, readableBuildInfo: incrementalReadableBuildInfo } = getBuildInfoForIncrementalCorrectnessCheck(incrementalBuildText);
400            const { buildInfo: cleanBuildInfo, readableBuildInfo: cleanReadableBuildInfo } = getBuildInfoForIncrementalCorrectnessCheck(cleanBuildText);
401            verifyTextEqual(incrementalBuildInfo, cleanBuildInfo, `TsBuild info text without affectedFilesPendingEmit:: ${outputFile}::`);
402                // Verify file info sigantures
403            verifyMapLike(
404                incrementalReadableBuildInfo?.program?.fileInfos as ReadableProgramMultiFileEmitBuildInfo["fileInfos"],
405                cleanReadableBuildInfo?.program?.fileInfos as ReadableProgramMultiFileEmitBuildInfo["fileInfos"],
406                (key, incrementalFileInfo, cleanFileInfo) => {
407                    if (incrementalFileInfo.signature !== cleanFileInfo.signature && incrementalFileInfo.signature !== incrementalFileInfo.version) {
408                        return [
409                            `Incremental signature is neither dts signature nor file version for File:: ${key}`,
410                            `Incremental:: ${JSON.stringify(incrementalFileInfo, /*replacer*/ undefined, 2)}`,
411                            `Clean:: ${JSON.stringify(cleanFileInfo, /*replacer*/ undefined, 2)}`
412                        ];
413                    }
414                },
415                `FileInfos:: File:: ${outputFile}`
416            );
417            if (!isReadableProgramBundleEmitBuildInfo(incrementalReadableBuildInfo?.program)) {
418                ts.Debug.assert(!isReadableProgramBundleEmitBuildInfo(cleanReadableBuildInfo?.program));
419                // Verify exportedModulesMap
420                verifyMapLike(
421                    incrementalReadableBuildInfo?.program?.exportedModulesMap,
422                    cleanReadableBuildInfo?.program?.exportedModulesMap,
423                    (key, incrementalReferenceSet, cleanReferenceSet) => {
424                        if (!ts.arrayIsEqualTo(incrementalReferenceSet, cleanReferenceSet) && !ts.arrayIsEqualTo(incrementalReferenceSet, (incrementalReadableBuildInfo!.program! as ReadableProgramMultiFileEmitBuildInfo).referencedMap![key])) {
425                            return [
426                                `Incremental Reference set is neither from dts nor files reference map for File:: ${key}::`,
427                                `Incremental:: ${JSON.stringify(incrementalReferenceSet, /*replacer*/ undefined, 2)}`,
428                                `Clean:: ${JSON.stringify(cleanReferenceSet, /*replacer*/ undefined, 2)}`,
429                                `IncrementalReferenceMap:: ${JSON.stringify((incrementalReadableBuildInfo!.program! as ReadableProgramMultiFileEmitBuildInfo).referencedMap![key], /*replacer*/ undefined, 2)}`,
430                                `CleanReferenceMap:: ${JSON.stringify((cleanReadableBuildInfo!.program! as ReadableProgramMultiFileEmitBuildInfo).referencedMap![key], /*replacer*/ undefined, 2)}`,
431                            ];
432                        }
433                    },
434                    `exportedModulesMap:: File:: ${outputFile}`
435                );
436                // Verify that incrementally pending affected file emit are in clean build since clean build can contain more files compared to incremental depending of noEmitOnError option
437                if (incrementalReadableBuildInfo?.program?.affectedFilesPendingEmit) {
438                    if (cleanReadableBuildInfo?.program?.affectedFilesPendingEmit === undefined) {
439                        addBaseline(
440                            `Incremental build contains affectedFilesPendingEmit, clean build does not have it: ${outputFile}::`,
441                            `Incremental buildInfoText:: ${incrementalBuildText}`,
442                            `Clean buildInfoText:: ${cleanBuildText}`
443                        );
444                    }
445                    let expectedIndex = 0;
446                    incrementalReadableBuildInfo.program.affectedFilesPendingEmit.forEach(([actualFile]) => {
447                        expectedIndex = ts.findIndex((cleanReadableBuildInfo!.program! as ReadableProgramMultiFileEmitBuildInfo).affectedFilesPendingEmit, ([expectedFile]) => actualFile === expectedFile, expectedIndex);
448                        if (expectedIndex === -1) {
449                            addBaseline(
450                                `Incremental build contains ${actualFile} file as pending emit, clean build does not have it: ${outputFile}::`,
451                                `Incremental buildInfoText:: ${incrementalBuildText}`,
452                                `Clean buildInfoText:: ${cleanBuildText}`
453                            );
454                        }
455                        expectedIndex++;
456                    });
457                }
458            }
459        }
460    }
461    if (!headerAdded && discrepancyExplanation) addBaseline("*** Supplied discrepancy explanation but didnt file any difference");
462    return baselines;
463
464    function verifyTextEqual(incrementalText: string | undefined, cleanText: string | undefined, message: string) {
465        if (incrementalText !== cleanText) writeNotEqual(incrementalText, cleanText, message);
466    }
467
468    function verifyMapLike<T>(incremental: ts.MapLike<T> | undefined, clean: ts.MapLike<T> | undefined, verifyValue: (key: string, incrementalValue: T, cleanValue: T) => string[] | undefined, message: string) {
469        verifyPresenceAbsence(incremental, clean, `Incremental and clean do not match:: ${message}`);
470        if (!incremental || !clean) return;
471        const incrementalMap = new ts.Map(ts.getEntries(incremental));
472        const cleanMap = new ts.Map(ts.getEntries(clean));
473        if (incrementalMap.size !== cleanMap.size) {
474            addBaseline(
475                `Incremental and clean size of maps do not match:: ${message}`,
476                `Incremental: ${JSON.stringify(incremental, /*replacer*/ undefined, 2)}`,
477                `Clean: ${JSON.stringify(clean, /*replacer*/ undefined, 2)}`,
478            );
479            return;
480        }
481        cleanMap.forEach((cleanValue, key) => {
482            const incrementalValue = incrementalMap.get(key);
483            if (!incrementalValue) {
484                addBaseline(
485                    `Incremental does not contain ${key} which is present in clean:: ${message}`,
486                    `Incremental: ${JSON.stringify(incremental, /*replacer*/ undefined, 2)}`,
487                    `Clean: ${JSON.stringify(clean, /*replacer*/ undefined, 2)}`,
488                );
489            }
490            else {
491                const result = verifyValue(key, incrementalMap.get(key)!, cleanValue);
492                if (result) addBaseline(...result);
493            }
494        });
495    }
496
497    function verifyPresenceAbsence<T>(actual: T | undefined, expected: T | undefined, message: string) {
498        if (expected === undefined) {
499            if (actual === undefined) return;
500        }
501        else {
502            if (actual !== undefined) return;
503        }
504        writeNotEqual(actual, expected, message);
505    }
506
507    function writeNotEqual<T>(actual: T | undefined, expected: T | undefined, message: string) {
508        addBaseline(
509            message,
510            "CleanBuild:",
511            ts.isString(expected) ? expected : JSON.stringify(expected),
512            "IncrementalBuild:",
513            ts.isString(actual) ? actual : JSON.stringify(actual),
514        );
515    }
516
517    function addBaseline(...text: string[]) {
518        if (!baselines || !headerAdded) {
519            (baselines ||= []).push(`${index}:: ${subScenario}`, ...(discrepancyExplanation?.()|| ["*** Needs explanation"]));
520            headerAdded = true;
521        }
522        baselines.push(...text);
523    }
524}
525
526function getBuildInfoForIncrementalCorrectnessCheck(text: string | undefined): {
527    buildInfo: string | undefined;
528    readableBuildInfo?: ReadableBuildInfo;
529} {
530    if (!text) return { buildInfo: text };
531    const readableBuildInfo = JSON.parse(text) as ReadableBuildInfo;
532    let sanitizedFileInfos: ts.MapLike<ReadableProgramBuildInfoFileInfo | string> | undefined;
533    if (readableBuildInfo.program?.fileInfos) {
534        sanitizedFileInfos = {};
535        for (const id in readableBuildInfo.program.fileInfos) {
536            if (ts.hasProperty(readableBuildInfo.program.fileInfos, id)) {
537                const info = readableBuildInfo.program.fileInfos[id];
538                sanitizedFileInfos[id] = ts.isString(info) ? info : { ...info, signature: undefined };
539            }
540        }
541    }
542    return {
543        buildInfo: JSON.stringify({
544            ...readableBuildInfo,
545            program: readableBuildInfo.program && {
546                ...readableBuildInfo.program,
547                fileNames: undefined,
548                fileNamesList: undefined,
549                fileInfos: sanitizedFileInfos,
550                // Ignore noEmit since that shouldnt be reason to emit the tsbuild info and presence of it in the buildinfo file does not matter
551                options: { ...readableBuildInfo.program.options, noEmit: undefined },
552                exportedModulesMap: undefined,
553                affectedFilesPendingEmit: undefined,
554                latestChangedDtsFile: readableBuildInfo.program.latestChangedDtsFile ? "FakeFileName" : undefined,
555            },
556            size: undefined, // Size doesnt need to be equal
557        },  /*replacer*/ undefined, 2),
558        readableBuildInfo,
559    };
560}
561
562export enum CleanBuildDescrepancy {
563    CleanFileTextDifferent,
564    CleanFilePresent,
565}
566
567export interface TestTscEdit {
568    modifyFs: (fs: vfs.FileSystem) => void;
569    subScenario: string;
570    commandLineArgs?: readonly string[];
571    /** An array of lines to be printed in order when a discrepancy is detected */
572    discrepancyExplanation?: () => readonly string[];
573}
574
575export interface VerifyTscWithEditsInput extends ts.TestTscCompile {
576    edits: TestTscEdit[];
577}
578
579/**
580 * Verify non watch tsc invokcation after each edit
581 */
582export function verifyTscWithEdits({
583    subScenario, fs, scenario, commandLineArgs,
584    baselineSourceMap, modifyFs, baselineReadFileCalls, baselinePrograms,
585    edits
586}: VerifyTscWithEditsInput) {
587    describe(`tsc ${commandLineArgs.join(" ")} ${scenario}:: ${subScenario} serializedEdits`, () => {
588        let sys: ts.TscCompileSystem;
589        let baseFs: vfs.FileSystem;
590        let editsSys: ts.TscCompileSystem[];
591        before(() => {
592            ts.Debug.assert(!!edits.length, `${scenario}/${subScenario}:: No incremental scenarios, you probably want to use verifyTsc instead.`);
593            baseFs = fs().makeReadonly();
594            sys = ts.testTscCompile({
595                scenario,
596                subScenario,
597                fs: () => baseFs,
598                commandLineArgs,
599                modifyFs,
600                baselineSourceMap,
601                baselineReadFileCalls,
602                baselinePrograms
603            });
604            edits.forEach((
605                { modifyFs, subScenario: editScenario, commandLineArgs: editCommandLineArgs },
606                index
607            ) => {
608                (editsSys || (editsSys = [])).push(ts.testTscCompile({
609                    scenario,
610                    subScenario: editScenario || subScenario,
611                    diffWithInitial: true,
612                    fs: () => index === 0 ? sys.vfs : editsSys[index - 1].vfs,
613                    commandLineArgs: editCommandLineArgs || commandLineArgs,
614                    modifyFs,
615                    baselineSourceMap,
616                    baselineReadFileCalls,
617                    baselinePrograms
618                }));
619            });
620        });
621        after(() => {
622            baseFs = undefined!;
623            sys = undefined!;
624            editsSys = undefined!;
625        });
626        ts.verifyTscBaseline(() => ({
627            baseLine: () => {
628                const { file, text } = sys.baseLine();
629                const texts: string[] = [text];
630                editsSys.forEach((sys, index) => {
631                    const incrementalScenario = edits[index];
632                    texts.push("");
633                    texts.push(`Change:: ${incrementalScenario.subScenario}`);
634                    texts.push(sys.baseLine().text);
635                });
636                return { file, text: texts.join("\r\n") };
637            }
638        }));
639        it("tsc invocation after edit and clean build correctness", () => {
640            let baselines: string[] | undefined;
641            for (let index = 0; index < edits.length; index++) {
642                baselines = verifyTscEditDiscrepancies({
643                    index,
644                    scenario,
645                    subScenario: edits[index].subScenario,
646                    baselines,
647                    baseFs,
648                    newSys: editsSys[index],
649                    commandLineArgs: edits[index].commandLineArgs || commandLineArgs,
650                    discrepancyExplanation: edits[index].discrepancyExplanation,
651                    editFs: fs => {
652                        for (let i = 0; i <= index; i++) {
653                            edits[i].modifyFs(fs);
654                        }
655                    },
656                    modifyFs
657                });
658            }
659            Harness.Baseline.runBaseline(
660                `${ts.isBuild(commandLineArgs) ? "tsbuild" : "tsc"}/${scenario}/${subScenario.split(" ").join("-")}-discrepancies.js`,
661                baselines ? baselines.join("\r\n") : null // eslint-disable-line no-null/no-null
662            );
663        });
664    });
665}
666
667export function enableStrict(fs: vfs.FileSystem, path: string) {
668    replaceText(fs, path, `"strict": false`, `"strict": true`);
669}
670
671export function addTestPrologue(fs: vfs.FileSystem, path: string, prologue: string) {
672    prependText(fs, path, `${prologue}
673`);
674}
675
676export function addShebang(fs: vfs.FileSystem, project: string, file: string) {
677    prependText(fs, `src/${project}/${file}.ts`, `#!someshebang ${project} ${file}
678`);
679}
680
681export function restContent(project: string, file: string) {
682    return `function for${project}${file}Rest() {
683const { b, ...rest } = { a: 10, b: 30, yy: 30 };
684}`;
685}
686
687function nonrestContent(project: string, file: string) {
688    return `function for${project}${file}Rest() { }`;
689}
690
691export function addRest(fs: vfs.FileSystem, project: string, file: string) {
692    appendText(fs, `src/${project}/${file}.ts`, restContent(project, file));
693}
694
695export function removeRest(fs: vfs.FileSystem, project: string, file: string) {
696    replaceText(fs, `src/${project}/${file}.ts`, restContent(project, file), nonrestContent(project, file));
697}
698
699export function addStubFoo(fs: vfs.FileSystem, project: string, file: string) {
700    appendText(fs, `src/${project}/${file}.ts`, nonrestContent(project, file));
701}
702
703export function changeStubToRest(fs: vfs.FileSystem, project: string, file: string) {
704    replaceText(fs, `src/${project}/${file}.ts`, nonrestContent(project, file), restContent(project, file));
705}
706
707export function addSpread(fs: vfs.FileSystem, project: string, file: string) {
708    const path = `src/${project}/${file}.ts`;
709    const content = fs.readFileSync(path, "utf8");
710    fs.writeFileSync(path, `${content}
711function ${project}${file}Spread(...b: number[]) { }
712const ${project}${file}_ar = [20, 30];
713${project}${file}Spread(10, ...${project}${file}_ar);`);
714
715    replaceText(fs, `src/${project}/tsconfig.json`, `"strict": false,`, `"strict": false,
716    "downlevelIteration": true,`);
717}
718
719export function getTripleSlashRef(project: string) {
720    return `/src/${project}/tripleRef.d.ts`;
721}
722
723export function addTripleSlashRef(fs: vfs.FileSystem, project: string, file: string) {
724    fs.writeFileSync(getTripleSlashRef(project), `declare class ${project}${file} { }`);
725    prependText(fs, `src/${project}/${file}.ts`, `///<reference path="./tripleRef.d.ts"/>
726const ${file}Const = new ${project}${file}();
727`);
728}
729