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