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