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