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.fileName, 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, ts.getDeclarationEmitExtensionForPath(sourceFile.fileName))), 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 140 public get vfs(): vfs.FileSystem { 141 return this.host.vfs; 142 } 143 144 public get inputs(): readonly documents.TextDocument[] { 145 return this._inputs; 146 } 147 148 public get outputs(): readonly documents.TextDocument[] { 149 return this.host.outputs; 150 } 151 152 public get traces(): readonly string[] { 153 return this.host.traces; 154 } 155 156 public get emitSkipped(): boolean { 157 return this.result && this.result.emitSkipped || false; 158 } 159 160 public get singleFile(): boolean { 161 return !!this.options.outFile || !!this.options.out; 162 } 163 164 public get commonSourceDirectory(): string { 165 const common = this.program && this.program.getCommonSourceDirectory() || ""; 166 return common && vpath.combine(this.vfs.cwd(), common); 167 } 168 169 public getInputsAndOutputs(path: string): CompilationOutput | undefined { 170 return this._inputsAndOutputs.get(vpath.resolve(this.vfs.cwd(), path)); 171 } 172 173 public getInputs(path: string): readonly documents.TextDocument[] | undefined { 174 const outputs = this.getInputsAndOutputs(path); 175 return outputs && outputs.inputs; 176 } 177 178 public getOutput(path: string, kind: "js" | "dts" | "map"): documents.TextDocument | undefined { 179 const outputs = this.getInputsAndOutputs(path); 180 return outputs && outputs[kind]; 181 } 182 183 public getSourceMapRecord(): string | undefined { 184 const maps = this.result!.sourceMaps; 185 if (maps && maps.length > 0) { 186 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())); 187 } 188 } 189 190 public getSourceMap(path: string): documents.SourceMap | undefined { 191 if (this.options.noEmit || vpath.isDeclaration(path)) return undefined; 192 if (this.options.inlineSourceMap) { 193 const document = this.getOutput(path, "js"); 194 return document && documents.SourceMap.fromSource(document.text); 195 } 196 if (this.options.sourceMap) { 197 const document = this.getOutput(path, "map"); 198 return document && new documents.SourceMap(document.file, document.text); 199 } 200 } 201 202 public getOutputPath(path: string, ext: string): string { 203 if (this.options.outFile || this.options.out) { 204 path = vpath.resolve(this.vfs.cwd(), this.options.outFile || this.options.out); 205 } 206 else { 207 path = vpath.resolve(this.vfs.cwd(), path); 208 const outDir = ext === ".d.ts" || ext === ".json.d.ts" || ext === ".d.mts" || ext === ".d.cts" ? this.options.declarationDir || this.options.outDir : this.options.outDir; 209 if (outDir) { 210 const common = this.commonSourceDirectory; 211 if (common) { 212 path = vpath.relative(common, path, this.vfs.ignoreCase); 213 path = vpath.combine(vpath.resolve(this.vfs.cwd(), this.options.outDir), path); 214 } 215 } 216 } 217 return vpath.changeExtension(path, ext); 218 } 219 220 public getNumberOfJsFiles(includeJson: boolean) { 221 if (includeJson) { 222 return this.js.size; 223 } 224 else { 225 let count = this.js.size; 226 this.js.forEach(document => { 227 if (ts.fileExtensionIs(document.file, ts.Extension.Json)) { 228 count--; 229 } 230 }); 231 return count; 232 } 233 } 234 } 235 236 export function compileFiles(host: fakes.CompilerHost, rootFiles: string[] | undefined, compilerOptions: ts.CompilerOptions): CompilationResult { 237 if (compilerOptions.project || !rootFiles || rootFiles.length === 0) { 238 const project = readProject(host.parseConfigHost, compilerOptions.project, compilerOptions); 239 if (project) { 240 if (project.errors && project.errors.length > 0) { 241 return new CompilationResult(host, compilerOptions, /*program*/ undefined, /*result*/ undefined, project.errors); 242 } 243 if (project.config) { 244 rootFiles = project.config.fileNames; 245 compilerOptions = project.config.options; 246 } 247 } 248 delete compilerOptions.project; 249 } 250 251 // establish defaults (aligns with old harness) 252 if (compilerOptions.target === undefined && compilerOptions.module !== ts.ModuleKind.Node16 && compilerOptions.module !== ts.ModuleKind.NodeNext) compilerOptions.target = ts.ScriptTarget.ES3; 253 if (compilerOptions.newLine === undefined) compilerOptions.newLine = ts.NewLineKind.CarriageReturnLineFeed; 254 if (compilerOptions.skipDefaultLibCheck === undefined) compilerOptions.skipDefaultLibCheck = true; 255 if (compilerOptions.noErrorTruncation === undefined) compilerOptions.noErrorTruncation = true; 256 257 // pre-emit/post-emit error comparison requires declaration emit twice, which can be slow. If it's unlikely to flag any error consistency issues 258 // and if the test is running `skipLibCheck` - an indicator that we want the tets to run quickly - skip the before/after error comparison, too 259 const skipErrorComparison = ts.length(rootFiles) >= 100 || (!!compilerOptions.skipLibCheck && !!compilerOptions.declaration); 260 261 const preProgram = !skipErrorComparison ? ts.createProgram(rootFiles || [], { ...compilerOptions, configFile: compilerOptions.configFile, traceResolution: false }, host) : undefined; 262 const preErrors = preProgram && ts.getPreEmitDiagnostics(preProgram); 263 264 const program = ts.createProgram(rootFiles || [], compilerOptions, host); 265 const emitResult = program.emit(); 266 const postErrors = ts.getPreEmitDiagnostics(program); 267 const longerErrors = ts.length(preErrors) > postErrors.length ? preErrors : postErrors; 268 const shorterErrors = longerErrors === preErrors ? postErrors : preErrors; 269 const errors = preErrors && (preErrors.length !== postErrors.length) ? [...shorterErrors!, 270 ts.addRelatedInfo( 271 ts.createCompilerDiagnostic({ 272 category: ts.DiagnosticCategory.Error, 273 code: -1, 274 key: "-1", 275 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!` 276 }), 277 ts.createCompilerDiagnostic({ 278 category: ts.DiagnosticCategory.Error, 279 code: -1, 280 key: "-1", 281 message: `The excess diagnostics are:` 282 }), 283 ...ts.filter(longerErrors!, p => !ts.some(shorterErrors, p2 => ts.compareDiagnostics(p, p2) === ts.Comparison.EqualTo)) 284 ) 285 ] : postErrors; 286 return new CompilationResult(host, compilerOptions, program, emitResult, errors); 287 } 288} 289