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