• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import * as ts from "./_namespaces/ts";
2import * as vfs from "./_namespaces/vfs";
3import * as Utils from "./_namespaces/Utils";
4import * as vpath from "./_namespaces/vpath";
5import * as documents from "./_namespaces/documents";
6import * as collections from "./_namespaces/collections";
7import * as Harness from "./_namespaces/Harness";
8
9/**
10 * Fake implementations of various compiler dependencies.
11 */
12
13const processExitSentinel = new Error("System exit");
14
15export interface SystemOptions {
16    executingFilePath?: string;
17    newLine?: "\r\n" | "\n";
18    env?: Record<string, string>;
19}
20
21/**
22 * A fake `ts.System` that leverages a virtual file system.
23 */
24export class System implements ts.System {
25    public readonly vfs: vfs.FileSystem;
26    public readonly args: string[] = [];
27    public readonly output: string[] = [];
28    public readonly newLine: string;
29    public readonly useCaseSensitiveFileNames: boolean;
30    public exitCode: number | undefined;
31
32    private readonly _executingFilePath: string | undefined;
33    private readonly _env: Record<string, string> | undefined;
34
35    constructor(vfs: vfs.FileSystem, { executingFilePath, newLine = "\r\n", env }: SystemOptions = {}) {
36        this.vfs = vfs.isReadonly ? vfs.shadow() : vfs;
37        this.useCaseSensitiveFileNames = !this.vfs.ignoreCase;
38        this.newLine = newLine;
39        this._executingFilePath = executingFilePath;
40        this._env = env;
41    }
42
43    private testTerminalWidth = Number.parseInt(this.getEnvironmentVariable("TS_TEST_TERMINAL_WIDTH"));
44    getWidthOfTerminal = Number.isNaN(this.testTerminalWidth) ? undefined : () => this.testTerminalWidth;
45
46    // Pretty output
47    writeOutputIsTTY() {
48        return true;
49    }
50
51    public write(message: string) {
52        if (ts.Debug.isDebugging) console.log(message);
53        this.output.push(message);
54    }
55
56    public readFile(path: string) {
57        try {
58            const content = this.vfs.readFileSync(path, "utf8");
59            return content === undefined ? undefined : Utils.removeByteOrderMark(content);
60        }
61        catch {
62            return undefined;
63        }
64    }
65
66    public writeFile(path: string, data: string, writeByteOrderMark?: boolean): void {
67        this.vfs.mkdirpSync(vpath.dirname(path));
68        this.vfs.writeFileSync(path, writeByteOrderMark ? Utils.addUTF8ByteOrderMark(data) : data);
69    }
70
71    public deleteFile(path: string) {
72        this.vfs.unlinkSync(path);
73    }
74
75    public fileExists(path: string) {
76        const stats = this._getStats(path);
77        return stats ? stats.isFile() : false;
78    }
79
80    public directoryExists(path: string) {
81        const stats = this._getStats(path);
82        return stats ? stats.isDirectory() : false;
83    }
84
85    public createDirectory(path: string): void {
86        this.vfs.mkdirpSync(path);
87    }
88
89    public getCurrentDirectory() {
90        return this.vfs.cwd();
91    }
92
93    public getDirectories(path: string) {
94        const result: string[] = [];
95        try {
96            for (const file of this.vfs.readdirSync(path)) {
97                if (this.vfs.statSync(vpath.combine(path, file)).isDirectory()) {
98                    result.push(file);
99                }
100            }
101        }
102        catch { /*ignore*/ }
103        return result;
104    }
105
106    public readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] {
107        return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, path => this.getAccessibleFileSystemEntries(path), path => this.realpath(path));
108    }
109
110    public getAccessibleFileSystemEntries(path: string): ts.FileSystemEntries {
111        const files: string[] = [];
112        const directories: string[] = [];
113        try {
114            for (const file of this.vfs.readdirSync(path)) {
115                try {
116                    const stats = this.vfs.statSync(vpath.combine(path, file));
117                    if (stats.isFile()) {
118                        files.push(file);
119                    }
120                    else if (stats.isDirectory()) {
121                        directories.push(file);
122                    }
123                }
124                catch { /*ignored*/ }
125            }
126        }
127        catch { /*ignored*/ }
128        return { files, directories };
129    }
130
131    public exit(exitCode?: number) {
132        this.exitCode = exitCode;
133        throw processExitSentinel;
134    }
135
136    public getFileSize(path: string) {
137        const stats = this._getStats(path);
138        return stats && stats.isFile() ? stats.size : 0;
139    }
140
141    public resolvePath(path: string) {
142        return vpath.resolve(this.vfs.cwd(), path);
143    }
144
145    public getExecutingFilePath() {
146        if (this._executingFilePath === undefined) return ts.notImplemented();
147        return this._executingFilePath;
148    }
149
150    public getModifiedTime(path: string) {
151        const stats = this._getStats(path);
152        return stats ? stats.mtime : undefined!; // TODO: GH#18217
153    }
154
155    public setModifiedTime(path: string, time: Date) {
156        this.vfs.utimesSync(path, time, time);
157    }
158
159    public createHash(data: string): string {
160        return `${ts.generateDjb2Hash(data)}-${data}`;
161    }
162
163    public realpath(path: string) {
164        try {
165            return this.vfs.realpathSync(path);
166        }
167        catch {
168            return path;
169        }
170    }
171
172    public getEnvironmentVariable(name: string): string {
173        return (this._env && this._env[name])!; // TODO: GH#18217
174    }
175
176    private _getStats(path: string) {
177        try {
178            return this.vfs.existsSync(path) ? this.vfs.statSync(path) : undefined;
179        }
180        catch {
181            return undefined;
182        }
183    }
184
185    now() {
186        return new Date(this.vfs.time());
187    }
188}
189
190/**
191 * A fake `ts.ParseConfigHost` that leverages a virtual file system.
192 */
193export class ParseConfigHost implements ts.ParseConfigHost {
194    public readonly sys: System;
195
196    constructor(sys: System | vfs.FileSystem) {
197        if (sys instanceof vfs.FileSystem) sys = new System(sys);
198        this.sys = sys;
199    }
200
201    public get vfs() {
202        return this.sys.vfs;
203    }
204
205    public get useCaseSensitiveFileNames() {
206        return this.sys.useCaseSensitiveFileNames;
207    }
208
209    public fileExists(fileName: string): boolean {
210        return this.sys.fileExists(fileName);
211    }
212
213    public directoryExists(directoryName: string): boolean {
214        return this.sys.directoryExists(directoryName);
215    }
216
217    public readFile(path: string): string | undefined {
218        return this.sys.readFile(path);
219    }
220
221    public readDirectory(path: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] {
222        return this.sys.readDirectory(path, extensions, excludes, includes, depth);
223    }
224}
225
226/**
227 * A fake `ts.CompilerHost` that leverages a virtual file system.
228 */
229export class CompilerHost implements ts.CompilerHost {
230    public readonly sys: System;
231    public readonly defaultLibLocation: string;
232    public readonly outputs: documents.TextDocument[] = [];
233    private readonly _outputsMap: collections.SortedMap<string, number>;
234    public readonly traces: string[] = [];
235    public readonly shouldAssertInvariants = !Harness.lightMode;
236
237    private _setParentNodes: boolean;
238    private _sourceFiles: collections.SortedMap<string, ts.SourceFile>;
239    private _parseConfigHost: ParseConfigHost | undefined;
240    private _newLine: string;
241
242    constructor(sys: System | vfs.FileSystem, options = ts.getDefaultCompilerOptions(), setParentNodes = false) {
243        if (sys instanceof vfs.FileSystem) sys = new System(sys);
244        this.sys = sys;
245        this.defaultLibLocation = sys.vfs.meta.get("defaultLibLocation") || "";
246        this._newLine = ts.getNewLineCharacter(options, () => this.sys.newLine);
247        this._sourceFiles = new collections.SortedMap<string, ts.SourceFile>({ comparer: sys.vfs.stringComparer, sort: "insertion" });
248        this._setParentNodes = setParentNodes;
249        this._outputsMap = new collections.SortedMap(this.vfs.stringComparer);
250    }
251
252    public get vfs() {
253        return this.sys.vfs;
254    }
255
256    public get parseConfigHost() {
257        return this._parseConfigHost || (this._parseConfigHost = new ParseConfigHost(this.sys));
258    }
259
260    public getCurrentDirectory(): string {
261        return this.sys.getCurrentDirectory();
262    }
263
264    public useCaseSensitiveFileNames(): boolean {
265        return this.sys.useCaseSensitiveFileNames;
266    }
267
268    public getNewLine(): string {
269        return this._newLine;
270    }
271
272    public getCanonicalFileName(fileName: string): string {
273        return this.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
274    }
275
276    public deleteFile(fileName: string) {
277        this.sys.deleteFile(fileName);
278    }
279
280    public fileExists(fileName: string): boolean {
281        return this.sys.fileExists(fileName);
282    }
283
284    public directoryExists(directoryName: string): boolean {
285        return this.sys.directoryExists(directoryName);
286    }
287
288    public getModifiedTime(fileName: string) {
289        return this.sys.getModifiedTime(fileName);
290    }
291
292    public setModifiedTime(fileName: string, time: Date) {
293        return this.sys.setModifiedTime(fileName, time);
294    }
295
296    public getDirectories(path: string): string[] {
297        return this.sys.getDirectories(path);
298    }
299
300    public readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] {
301        return this.sys.readDirectory(path, extensions, exclude, include, depth);
302    }
303
304    public readFile(path: string): string | undefined {
305        return this.sys.readFile(path);
306    }
307
308    public writeFile(fileName: string, content: string, writeByteOrderMark: boolean) {
309        if (writeByteOrderMark) content = Utils.addUTF8ByteOrderMark(content);
310        this.sys.writeFile(fileName, content);
311
312        const document = new documents.TextDocument(fileName, content);
313        document.meta.set("fileName", fileName);
314        this.vfs.filemeta(fileName).set("document", document);
315        if (!this._outputsMap.has(document.file)) {
316            this._outputsMap.set(document.file, this.outputs.length);
317            this.outputs.push(document);
318        }
319        this.outputs[this._outputsMap.get(document.file)!] = document;
320    }
321
322    public trace(s: string): void {
323        this.traces.push(s);
324    }
325
326    public realpath(path: string): string {
327        return this.sys.realpath(path);
328    }
329
330    public getDefaultLibLocation(): string {
331        return vpath.resolve(this.getCurrentDirectory(), this.defaultLibLocation);
332    }
333
334    public getDefaultLibFileName(options: ts.CompilerOptions): string {
335        return vpath.resolve(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options));
336    }
337
338    public getSourceFile(fileName: string, languageVersion: number, _onError?: (message: string) => void, _shouldCreateNewSourceFile?: boolean, options?: ts.CompilerOptions): ts.SourceFile | undefined {
339        const canonicalFileName = this.getCanonicalFileName(vpath.resolve(this.getCurrentDirectory(), fileName));
340        const existing = this._sourceFiles.get(canonicalFileName);
341        if (existing) return existing;
342
343        const content = this.readFile(canonicalFileName);
344        if (content === undefined) return undefined;
345
346        // A virtual file system may shadow another existing virtual file system. This
347        // allows us to reuse a common virtual file system structure across multiple
348        // tests. If a virtual file is a shadow, it is likely that the file will be
349        // reused across multiple tests. In that case, we cache the SourceFile we parse
350        // so that it can be reused across multiple tests to avoid the cost of
351        // repeatedly parsing the same file over and over (such as lib.d.ts).
352        const cacheKey = this.vfs.shadowRoot && `SourceFile[languageVersion=${languageVersion},setParentNodes=${this._setParentNodes}]`;
353        if (cacheKey) {
354            const meta = this.vfs.filemeta(canonicalFileName);
355            const sourceFileFromMetadata = meta.get(cacheKey) as ts.SourceFile | undefined;
356            if (sourceFileFromMetadata && sourceFileFromMetadata.getFullText() === content) {
357                this._sourceFiles.set(canonicalFileName, sourceFileFromMetadata);
358                return sourceFileFromMetadata;
359            }
360        }
361
362        const parsed = ts.createSourceFile(fileName, content, languageVersion, this._setParentNodes || this.shouldAssertInvariants, undefined, options);
363        if (this.shouldAssertInvariants) {
364            Utils.assertInvariants(parsed, /*parent*/ undefined);
365        }
366
367        this._sourceFiles.set(canonicalFileName, parsed);
368
369        if (cacheKey) {
370            // store the cached source file on the unshadowed file with the same version.
371            const stats = this.vfs.statSync(canonicalFileName);
372
373            let fs = this.vfs;
374            while (fs.shadowRoot) {
375                try {
376                    const shadowRootStats = fs.shadowRoot.existsSync(canonicalFileName) ? fs.shadowRoot.statSync(canonicalFileName) : undefined!; // TODO: GH#18217
377                    if (shadowRootStats.dev !== stats.dev ||
378                        shadowRootStats.ino !== stats.ino ||
379                        shadowRootStats.mtimeMs !== stats.mtimeMs) {
380                        break;
381                    }
382
383                    fs = fs.shadowRoot;
384                }
385                catch {
386                    break;
387                }
388            }
389
390            if (fs !== this.vfs) {
391                fs.filemeta(canonicalFileName).set(cacheKey, parsed);
392            }
393        }
394
395        return parsed;
396    }
397}
398
399export type ExpectedDiagnosticMessage = [ts.DiagnosticMessage, ...(string | number)[]];
400export interface ExpectedDiagnosticMessageChain {
401    message: ExpectedDiagnosticMessage;
402    next?: ExpectedDiagnosticMessageChain[];
403}
404
405export interface ExpectedDiagnosticLocation {
406    file: string;
407    start: number;
408    length: number;
409}
410export interface ExpectedDiagnosticRelatedInformation extends ExpectedDiagnosticMessageChain {
411    location?: ExpectedDiagnosticLocation;
412}
413
414export enum DiagnosticKind {
415    Error = "Error",
416    Status = "Status"
417}
418export interface ExpectedErrorDiagnostic extends ExpectedDiagnosticRelatedInformation {
419    relatedInformation?: ExpectedDiagnosticRelatedInformation[];
420}
421
422export type ExpectedDiagnostic = ExpectedDiagnosticMessage | ExpectedErrorDiagnostic;
423
424interface SolutionBuilderDiagnostic {
425    kind: DiagnosticKind;
426    diagnostic: ts.Diagnostic;
427}
428
429function indentedText(indent: number, text: string) {
430    if (!indent) return text;
431    let indentText = "";
432    for (let i = 0; i < indent; i++) {
433        indentText += "  ";
434    }
435    return `
436${indentText}${text}`;
437}
438
439function expectedDiagnosticMessageToText([message, ...args]: ExpectedDiagnosticMessage) {
440    let text = ts.getLocaleSpecificMessage(message);
441    if (args.length) {
442        text = ts.formatStringFromArgs(text, args);
443    }
444    return text;
445}
446
447function expectedDiagnosticMessageChainToText({ message, next }: ExpectedDiagnosticMessageChain, indent = 0) {
448    let text = indentedText(indent, expectedDiagnosticMessageToText(message));
449    if (next) {
450        indent++;
451        next.forEach(kid => text += expectedDiagnosticMessageChainToText(kid, indent));
452    }
453    return text;
454}
455
456function expectedDiagnosticRelatedInformationToText({ location, ...diagnosticMessage }: ExpectedDiagnosticRelatedInformation) {
457    const text = expectedDiagnosticMessageChainToText(diagnosticMessage);
458    if (location) {
459        const { file, start, length } = location;
460        return `${file}(${start}:${length}):: ${text}`;
461    }
462    return text;
463}
464
465function expectedErrorDiagnosticToText({ relatedInformation, ...diagnosticRelatedInformation }: ExpectedErrorDiagnostic) {
466    let text = `${DiagnosticKind.Error}!: ${expectedDiagnosticRelatedInformationToText(diagnosticRelatedInformation)}`;
467    if (relatedInformation) {
468        for (const kid of relatedInformation) {
469            text += `
470  related:: ${expectedDiagnosticRelatedInformationToText(kid)}`;
471        }
472    }
473    return text;
474}
475
476function expectedDiagnosticToText(errorOrStatus: ExpectedDiagnostic) {
477    return ts.isArray(errorOrStatus) ?
478        `${DiagnosticKind.Status}!: ${expectedDiagnosticMessageToText(errorOrStatus)}` :
479        expectedErrorDiagnosticToText(errorOrStatus);
480}
481
482function diagnosticMessageChainToText({ messageText, next}: ts.DiagnosticMessageChain, indent = 0) {
483    let text = indentedText(indent, messageText);
484    if (next) {
485        indent++;
486        next.forEach(kid => text += diagnosticMessageChainToText(kid, indent));
487    }
488    return text;
489}
490
491function diagnosticRelatedInformationToText({ file, start, length, messageText }: ts.DiagnosticRelatedInformation) {
492    const text = typeof messageText === "string" ?
493        messageText :
494        diagnosticMessageChainToText(messageText);
495    return file ?
496        `${file.fileName}(${start}:${length}):: ${text}` :
497        text;
498}
499
500function diagnosticToText({ kind, diagnostic: { relatedInformation, ...diagnosticRelatedInformation } }: SolutionBuilderDiagnostic) {
501    let text = `${kind}!: ${diagnosticRelatedInformationToText(diagnosticRelatedInformation)}`;
502    if (relatedInformation) {
503        for (const kid of relatedInformation) {
504            text += `
505  related:: ${diagnosticRelatedInformationToText(kid)}`;
506        }
507    }
508    return text;
509}
510
511export const version = "FakeTSVersion";
512
513export function patchHostForBuildInfoReadWrite<T extends ts.System>(sys: T) {
514    const originalReadFile = sys.readFile;
515    sys.readFile = (path, encoding) => {
516        const value = originalReadFile.call(sys, path, encoding);
517        if (!value || !ts.isBuildInfoFile(path)) return value;
518        const buildInfo = ts.getBuildInfo(path, value);
519        if (!buildInfo) return value;
520        ts.Debug.assert(buildInfo.version === version);
521        buildInfo.version = ts.version;
522        return ts.getBuildInfoText(buildInfo);
523    };
524    return patchHostForBuildInfoWrite(sys, version);
525}
526
527export function patchHostForBuildInfoWrite<T extends ts.System>(sys: T, version: string) {
528    const originalWrite = sys.write;
529    sys.write = msg => originalWrite.call(sys, msg.replace(ts.version, version));
530    const originalWriteFile = sys.writeFile;
531    sys.writeFile = (fileName: string, content: string, writeByteOrderMark: boolean) => {
532        if (ts.isBuildInfoFile(fileName)) {
533            const buildInfo = ts.getBuildInfo(fileName, content);
534            if (buildInfo) {
535                buildInfo.version = version;
536                return originalWriteFile.call(sys, fileName, ts.getBuildInfoText(buildInfo), writeByteOrderMark);
537            }
538        }
539        return originalWriteFile.call(sys, fileName, content, writeByteOrderMark);
540    };
541    return sys;
542}
543
544export class SolutionBuilderHost extends CompilerHost implements ts.SolutionBuilderHost<ts.BuilderProgram> {
545    createProgram: ts.CreateProgram<ts.BuilderProgram>;
546
547    private constructor(sys: System | vfs.FileSystem, options?: ts.CompilerOptions, setParentNodes?: boolean, createProgram?: ts.CreateProgram<ts.BuilderProgram>) {
548        super(sys, options, setParentNodes);
549        this.createProgram = createProgram || ts.createEmitAndSemanticDiagnosticsBuilderProgram;
550    }
551
552    static create(sys: System | vfs.FileSystem, options?: ts.CompilerOptions, setParentNodes?: boolean, createProgram?: ts.CreateProgram<ts.BuilderProgram>) {
553        const host = new SolutionBuilderHost(sys, options, setParentNodes, createProgram);
554        patchHostForBuildInfoReadWrite(host.sys);
555        return host;
556    }
557
558    createHash(data: string) {
559        return `${ts.generateDjb2Hash(data)}-${data}`;
560    }
561
562    diagnostics: SolutionBuilderDiagnostic[] = [];
563
564    reportDiagnostic(diagnostic: ts.Diagnostic) {
565        this.diagnostics.push({ kind: DiagnosticKind.Error, diagnostic });
566    }
567
568    reportSolutionBuilderStatus(diagnostic: ts.Diagnostic) {
569        this.diagnostics.push({ kind: DiagnosticKind.Status, diagnostic });
570    }
571
572    clearDiagnostics() {
573        this.diagnostics.length = 0;
574    }
575
576    assertDiagnosticMessages(...expectedDiagnostics: ExpectedDiagnostic[]) {
577        const actual = this.diagnostics.slice().map(diagnosticToText);
578        const expected = expectedDiagnostics.map(expectedDiagnosticToText);
579        assert.deepEqual(actual, expected, `Diagnostic arrays did not match:
580Actual: ${JSON.stringify(actual, /*replacer*/ undefined, " ")}
581Expected: ${JSON.stringify(expected, /*replacer*/ undefined, " ")}`);
582    }
583
584    assertErrors(...expectedDiagnostics: ExpectedErrorDiagnostic[]) {
585        const actual = this.diagnostics.filter(d => d.kind === DiagnosticKind.Error).map(diagnosticToText);
586        const expected = expectedDiagnostics.map(expectedDiagnosticToText);
587        assert.deepEqual(actual, expected, `Diagnostics arrays did not match:
588Actual: ${JSON.stringify(actual, /*replacer*/ undefined, " ")}
589Expected: ${JSON.stringify(expected, /*replacer*/ undefined, " ")}
590Actual All:: ${JSON.stringify(this.diagnostics.slice().map(diagnosticToText), /*replacer*/ undefined, " ")}`);
591    }
592
593    printDiagnostics(header = "== Diagnostics ==") {
594        const out = ts.createDiagnosticReporter(ts.sys);
595        ts.sys.write(header + "\r\n");
596        for (const { diagnostic } of this.diagnostics) {
597            out(diagnostic);
598        }
599    }
600
601    now() {
602        return this.sys.now();
603    }
604}
605
606