• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * Test harness compiler functionality.
3 */
4namespace compiler {
5    export interface Project {
6        file: string;
7        config?: ts.ParsedCommandLine;
8        errors?: ts.Diagnostic[];
9    }
10
11    export function readProject(host: fakes.ParseConfigHost, project: string | undefined, existingOptions?: ts.CompilerOptions): Project | undefined {
12        if (project) {
13            project = vpath.isTsConfigFile(project) ? project : vpath.combine(project, "tsconfig.json");
14        }
15        else {
16            [project] = host.vfs.scanSync(".", "ancestors-or-self", {
17                accept: (path, stats) => stats.isFile() && host.vfs.stringComparer(vpath.basename(path), "tsconfig.json") === 0
18            });
19        }
20
21        if (project) {
22            // TODO(rbuckton): Do we need to resolve this? Resolving breaks projects tests.
23            // project = vpath.resolve(host.vfs.currentDirectory, project);
24
25            // read the config file
26            const readResult = ts.readConfigFile(project, path => host.readFile(path));
27            if (readResult.error) {
28                return { file: project, errors: [readResult.error] };
29            }
30
31            // parse the config file
32            const config = ts.parseJsonConfigFileContent(readResult.config, host, vpath.dirname(project), existingOptions);
33            return { file: project, errors: config.errors, config };
34        }
35    }
36
37    /**
38     * Correlates compilation inputs and outputs
39     */
40    export interface CompilationOutput {
41        readonly inputs: readonly documents.TextDocument[];
42        readonly js: documents.TextDocument | undefined;
43        readonly dts: documents.TextDocument | undefined;
44        readonly map: documents.TextDocument | undefined;
45    }
46
47    export class CompilationResult {
48        public readonly host: fakes.CompilerHost;
49        public readonly program: ts.Program | undefined;
50        public readonly result: ts.EmitResult | undefined;
51        public readonly options: ts.CompilerOptions;
52        public readonly diagnostics: readonly ts.Diagnostic[];
53        public readonly js: ReadonlyMap<string, documents.TextDocument>;
54        public readonly dts: ReadonlyMap<string, documents.TextDocument>;
55        public readonly maps: ReadonlyMap<string, documents.TextDocument>;
56        public symlinks?: vfs.FileSet; // Location to store original symlinks so they may be used in both original and declaration file compilations
57
58        private _inputs: documents.TextDocument[] = [];
59        private _inputsAndOutputs: collections.SortedMap<string, CompilationOutput>;
60
61        constructor(host: fakes.CompilerHost, options: ts.CompilerOptions, program: ts.Program | undefined, result: ts.EmitResult | undefined, diagnostics: readonly ts.Diagnostic[]) {
62            this.host = host;
63            this.program = program;
64            this.result = result;
65            this.diagnostics = diagnostics;
66            this.options = program ? program.getCompilerOptions() : options;
67
68            // collect outputs
69            const js = this.js = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
70            const dts = this.dts = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
71            const maps = this.maps = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
72            for (const document of this.host.outputs) {
73                if (vpath.isJavaScript(document.file) || ts.fileExtensionIs(document.file, ts.Extension.Json)) {
74                    js.set(document.file, document);
75                }
76                else if (vpath.isDeclaration(document.file)) {
77                    dts.set(document.file, document);
78                }
79                else if (vpath.isSourceMap(document.file)) {
80                    maps.set(document.file, document);
81                }
82            }
83
84            // correlate inputs and outputs
85            this._inputsAndOutputs = new collections.SortedMap<string, CompilationOutput>({ comparer: this.vfs.stringComparer, sort: "insertion" });
86            if (program) {
87                if (this.options.out || this.options.outFile) {
88                    const outFile = vpath.resolve(this.vfs.cwd(), this.options.outFile || this.options.out);
89                    const inputs: documents.TextDocument[] = [];
90                    for (const sourceFile of program.getSourceFiles()) {
91                        if (sourceFile) {
92                            const input = new documents.TextDocument(sourceFile.fileName, sourceFile.text);
93                            this._inputs.push(input);
94                            if (!vpath.isDeclaration(sourceFile.fileName)) {
95                                inputs.push(input);
96                            }
97                        }
98                    }
99
100                    const outputs: CompilationOutput = {
101                        inputs,
102                        js: js.get(outFile),
103                        dts: dts.get(vpath.changeExtension(outFile, ".d.ts")),
104                        map: maps.get(outFile + ".map")
105                    };
106
107                    if (outputs.js) this._inputsAndOutputs.set(outputs.js.file, outputs);
108                    if (outputs.dts) this._inputsAndOutputs.set(outputs.dts.file, outputs);
109                    if (outputs.map) this._inputsAndOutputs.set(outputs.map.file, outputs);
110
111                    for (const input of inputs) {
112                        this._inputsAndOutputs.set(input.file, outputs);
113                    }
114                }
115                else {
116                    for (const sourceFile of program.getSourceFiles()) {
117                        if (sourceFile) {
118                            const input = new documents.TextDocument(sourceFile.fileName, sourceFile.text);
119                            this._inputs.push(input);
120                            if (!vpath.isDeclaration(sourceFile.fileName)) {
121                                const extname = ts.getOutputExtension(sourceFile, this.options);
122                                const outputs: CompilationOutput = {
123                                    inputs: [input],
124                                    js: js.get(this.getOutputPath(sourceFile.fileName, extname)),
125                                    dts: dts.get(this.getOutputPath(sourceFile.fileName, ".d.ts")),
126                                    map: maps.get(this.getOutputPath(sourceFile.fileName, extname + ".map"))
127                                };
128
129                                this._inputsAndOutputs.set(sourceFile.fileName, outputs);
130                                if (outputs.js) this._inputsAndOutputs.set(outputs.js.file, outputs);
131                                if (outputs.dts) this._inputsAndOutputs.set(outputs.dts.file, outputs);
132                                if (outputs.map) this._inputsAndOutputs.set(outputs.map.file, outputs);
133                            }
134                        }
135                    }
136                }
137            }
138
139            this.diagnostics = diagnostics;
140        }
141
142        public get vfs(): vfs.FileSystem {
143            return this.host.vfs;
144        }
145
146        public get inputs(): readonly documents.TextDocument[] {
147            return this._inputs;
148        }
149
150        public get outputs(): readonly documents.TextDocument[] {
151            return this.host.outputs;
152        }
153
154        public get traces(): readonly string[] {
155            return this.host.traces;
156        }
157
158        public get emitSkipped(): boolean {
159            return this.result && this.result.emitSkipped || false;
160        }
161
162        public get singleFile(): boolean {
163            return !!this.options.outFile || !!this.options.out;
164        }
165
166        public get commonSourceDirectory(): string {
167            const common = this.program && this.program.getCommonSourceDirectory() || "";
168            return common && vpath.combine(this.vfs.cwd(), common);
169        }
170
171        public getInputsAndOutputs(path: string): CompilationOutput | undefined {
172            return this._inputsAndOutputs.get(vpath.resolve(this.vfs.cwd(), path));
173        }
174
175        public getInputs(path: string): readonly documents.TextDocument[] | undefined {
176            const outputs = this.getInputsAndOutputs(path);
177            return outputs && outputs.inputs;
178        }
179
180        public getOutput(path: string, kind: "js" | "dts" | "map"): documents.TextDocument | undefined {
181            const outputs = this.getInputsAndOutputs(path);
182            return outputs && outputs[kind];
183        }
184
185        public getSourceMapRecord(): string | undefined {
186            const maps = this.result!.sourceMaps;
187            if (maps && maps.length > 0) {
188                return Harness.SourceMapRecorder.getSourceMapRecord(maps, this.program!, Array.from(this.js.values()).filter(d => !ts.fileExtensionIs(d.file, ts.Extension.Json)), Array.from(this.dts.values()));
189            }
190        }
191
192        public getSourceMap(path: string): documents.SourceMap | undefined {
193            if (this.options.noEmit || vpath.isDeclaration(path)) return undefined;
194            if (this.options.inlineSourceMap) {
195                const document = this.getOutput(path, "js");
196                return document && documents.SourceMap.fromSource(document.text);
197            }
198            if (this.options.sourceMap) {
199                const document = this.getOutput(path, "map");
200                return document && new documents.SourceMap(document.file, document.text);
201            }
202        }
203
204        public getOutputPath(path: string, ext: string): string {
205            if (this.options.outFile || this.options.out) {
206                path = vpath.resolve(this.vfs.cwd(), this.options.outFile || this.options.out);
207            }
208            else {
209                path = vpath.resolve(this.vfs.cwd(), path);
210                const outDir = ext === ".d.ts" ? this.options.declarationDir || this.options.outDir : this.options.outDir;
211                if (outDir) {
212                    const common = this.commonSourceDirectory;
213                    if (common) {
214                        path = vpath.relative(common, path, this.vfs.ignoreCase);
215                        path = vpath.combine(vpath.resolve(this.vfs.cwd(), this.options.outDir), path);
216                    }
217                }
218            }
219            return vpath.changeExtension(path, ext);
220        }
221
222        public getNumberOfJsFiles(includeJson: boolean) {
223            if (includeJson) {
224                return this.js.size;
225            }
226            else {
227                let count = this.js.size;
228                this.js.forEach(document => {
229                    if (ts.fileExtensionIs(document.file, ts.Extension.Json)) {
230                        count--;
231                    }
232                });
233                return count;
234            }
235        }
236    }
237
238    export function compileFiles(host: fakes.CompilerHost, rootFiles: string[] | undefined, compilerOptions: ts.CompilerOptions): CompilationResult {
239        if (compilerOptions.project || !rootFiles || rootFiles.length === 0) {
240            const project = readProject(host.parseConfigHost, compilerOptions.project, compilerOptions);
241            if (project) {
242                if (project.errors && project.errors.length > 0) {
243                    return new CompilationResult(host, compilerOptions, /*program*/ undefined, /*result*/ undefined, project.errors);
244                }
245                if (project.config) {
246                    rootFiles = project.config.fileNames;
247                    compilerOptions = project.config.options;
248                }
249            }
250            delete compilerOptions.project;
251        }
252
253        // establish defaults (aligns with old harness)
254        if (compilerOptions.target === undefined) compilerOptions.target = ts.ScriptTarget.ES3;
255        if (compilerOptions.newLine === undefined) compilerOptions.newLine = ts.NewLineKind.CarriageReturnLineFeed;
256        if (compilerOptions.skipDefaultLibCheck === undefined) compilerOptions.skipDefaultLibCheck = true;
257        if (compilerOptions.noErrorTruncation === undefined) compilerOptions.noErrorTruncation = true;
258
259        const preProgram = ts.length(rootFiles) < 100 ? ts.createProgram(rootFiles || [], { ...compilerOptions, configFile: compilerOptions.configFile, traceResolution: false }, host) : undefined;
260        const preErrors = preProgram && ts.getPreEmitDiagnostics(preProgram);
261
262        const program = ts.createProgram(rootFiles || [], compilerOptions, host);
263        const emitResult = program.emit();
264        const postErrors = ts.getPreEmitDiagnostics(program);
265        const errors = preErrors && (preErrors.length !== postErrors.length) ? [...postErrors,
266            ts.addRelatedInfo(
267                ts.createCompilerDiagnostic({
268                    category: ts.DiagnosticCategory.Error,
269                    code: -1,
270                    key: "-1",
271                    message: `Pre-emit (${preErrors.length}) and post-emit (${postErrors.length}) diagnostic counts do not match! This can indicate that a semantic _error_ was added by the emit resolver - such an error may not be reflected on the command line or in the editor, but may be captured in a baseline here!`
272                }),
273                ts.createCompilerDiagnostic({
274                    category: ts.DiagnosticCategory.Error,
275                    code: -1,
276                    key: "-1",
277                    message: `The excess diagnostics are:`
278                }),
279                ...ts.filter(postErrors, p => !ts.some(preErrors, p2 => ts.compareDiagnostics(p, p2) === ts.Comparison.EqualTo))
280            )
281        ] : postErrors;
282        return new CompilationResult(host, compilerOptions, program, emitResult, errors);
283    }
284}
285