TypeScript/src/harness/compilerImpl.ts

289 lines
15 KiB
TypeScript
Raw Permalink Normal View History

2018-04-24 19:48:55 +02:00
/**
* Test harness compiler functionality.
*/
namespace compiler {
2017-11-10 01:01:33 +01:00
export interface Project {
file: string;
config?: ts.ParsedCommandLine;
errors?: ts.Diagnostic[];
}
2018-04-19 20:30:03 +02:00
export function readProject(host: fakes.ParseConfigHost, project: string | undefined, existingOptions?: ts.CompilerOptions): Project | undefined {
2017-11-10 01:01:33 +01:00
if (project) {
project = vpath.isTsConfigFile(project) ? project : vpath.combine(project, "tsconfig.json");
2017-11-10 01:01:33 +01:00
}
else {
2018-01-23 19:27:20 +01:00
[project] = host.vfs.scanSync(".", "ancestors-or-self", {
2018-01-23 19:40:56 +01:00
accept: (path, stats) => stats.isFile() && host.vfs.stringComparer(vpath.basename(path), "tsconfig.json") === 0
2018-01-23 19:27:20 +01:00
});
2017-11-10 01:01:33 +01:00
}
if (project) {
// TODO(rbuckton): Do we need to resolve this? Resolving breaks projects tests.
// project = vpath.resolve(host.vfs.currentDirectory, project);
// read the config file
const readResult = ts.readConfigFile(project, path => host.readFile(path));
if (readResult.error) {
return { file: project, errors: [readResult.error] };
}
// parse the config file
const config = ts.parseJsonConfigFileContent(readResult.config, host, vpath.dirname(project), existingOptions);
return { file: project, errors: config.errors, config };
}
}
2017-12-21 20:20:44 +01:00
/**
* Correlates compilation inputs and outputs
*/
2017-11-10 01:01:33 +01:00
export interface CompilationOutput {
readonly inputs: readonly documents.TextDocument[];
readonly js: documents.TextDocument | undefined;
readonly dts: documents.TextDocument | undefined;
readonly map: documents.TextDocument | undefined;
2017-11-10 01:01:33 +01:00
}
export class CompilationResult {
2018-04-19 20:30:03 +02:00
public readonly host: fakes.CompilerHost;
2017-11-10 01:01:33 +01:00
public readonly program: ts.Program | undefined;
public readonly result: ts.EmitResult | undefined;
public readonly options: ts.CompilerOptions;
public readonly diagnostics: readonly ts.Diagnostic[];
2018-01-30 20:57:47 +01:00
public readonly js: ReadonlyMap<string, documents.TextDocument>;
public readonly dts: ReadonlyMap<string, documents.TextDocument>;
public readonly maps: ReadonlyMap<string, documents.TextDocument>;
public symlinks?: vfs.FileSet; // Location to store original symlinks so they may be used in both original and declaration file compilations
2017-11-10 01:01:33 +01:00
private _inputs: documents.TextDocument[] = [];
2018-04-24 19:48:55 +02:00
private _inputsAndOutputs: collections.SortedMap<string, CompilationOutput>;
2017-11-10 01:01:33 +01:00
constructor(host: fakes.CompilerHost, options: ts.CompilerOptions, program: ts.Program | undefined, result: ts.EmitResult | undefined, diagnostics: readonly ts.Diagnostic[]) {
2017-11-10 01:01:33 +01:00
this.host = host;
this.program = program;
this.result = result;
this.diagnostics = diagnostics;
this.options = program ? program.getCompilerOptions() : options;
// collect outputs
2018-04-24 19:48:55 +02:00
const js = this.js = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
const dts = this.dts = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
const maps = this.maps = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
2017-11-10 01:01:33 +01:00
for (const document of this.host.outputs) {
2018-05-04 00:35:54 +02:00
if (vpath.isJavaScript(document.file) || ts.fileExtensionIs(document.file, ts.Extension.Json)) {
2017-11-10 22:52:12 +01:00
js.set(document.file, document);
2017-11-10 01:01:33 +01:00
}
2018-04-20 01:47:07 +02:00
else if (vpath.isDeclaration(document.file)) {
2017-11-10 22:52:12 +01:00
dts.set(document.file, document);
2017-11-10 01:01:33 +01:00
}
2018-04-20 01:47:07 +02:00
else if (vpath.isSourceMap(document.file)) {
2017-11-10 22:52:12 +01:00
maps.set(document.file, document);
2017-11-10 01:01:33 +01:00
}
}
// correlate inputs and outputs
2018-04-24 19:48:55 +02:00
this._inputsAndOutputs = new collections.SortedMap<string, CompilationOutput>({ comparer: this.vfs.stringComparer, sort: "insertion" });
2017-11-10 01:01:33 +01:00
if (program) {
2017-12-21 20:20:44 +01:00
if (this.options.out || this.options.outFile) {
const outFile = vpath.resolve(this.vfs.cwd(), this.options.outFile || this.options.out);
2017-12-21 20:20:44 +01:00
const inputs: documents.TextDocument[] = [];
for (const sourceFile of program.getSourceFiles()) {
if (sourceFile) {
const input = new documents.TextDocument(sourceFile.fileName, sourceFile.text);
this._inputs.push(input);
2018-04-20 01:47:07 +02:00
if (!vpath.isDeclaration(sourceFile.fileName)) {
2017-12-21 20:20:44 +01:00
inputs.push(input);
}
}
}
const outputs: CompilationOutput = {
inputs,
js: js.get(outFile),
dts: dts.get(vpath.changeExtension(outFile, ".d.ts")),
map: maps.get(outFile + ".map")
};
if (outputs.js) this._inputsAndOutputs.set(outputs.js.file, outputs);
if (outputs.dts) this._inputsAndOutputs.set(outputs.dts.file, outputs);
if (outputs.map) this._inputsAndOutputs.set(outputs.map.file, outputs);
for (const input of inputs) {
this._inputsAndOutputs.set(input.file, outputs);
}
}
else {
for (const sourceFile of program.getSourceFiles()) {
if (sourceFile) {
const input = new documents.TextDocument(sourceFile.fileName, sourceFile.text);
this._inputs.push(input);
2018-04-20 01:47:07 +02:00
if (!vpath.isDeclaration(sourceFile.fileName)) {
const extname = ts.getOutputExtension(sourceFile.fileName, this.options);
2017-12-21 20:20:44 +01:00
const outputs: CompilationOutput = {
inputs: [input],
js: js.get(this.getOutputPath(sourceFile.fileName, extname)),
dts: dts.get(this.getOutputPath(sourceFile.fileName, ts.getDeclarationEmitExtensionForPath(sourceFile.fileName))),
2017-12-21 20:20:44 +01:00
map: maps.get(this.getOutputPath(sourceFile.fileName, extname + ".map"))
};
this._inputsAndOutputs.set(sourceFile.fileName, outputs);
if (outputs.js) this._inputsAndOutputs.set(outputs.js.file, outputs);
if (outputs.dts) this._inputsAndOutputs.set(outputs.dts.file, outputs);
if (outputs.map) this._inputsAndOutputs.set(outputs.map.file, outputs);
}
2017-11-10 01:01:33 +01:00
}
}
}
}
}
public get vfs(): vfs.FileSystem {
2017-11-10 01:01:33 +01:00
return this.host.vfs;
}
public get inputs(): readonly documents.TextDocument[] {
2017-11-10 01:01:33 +01:00
return this._inputs;
}
public get outputs(): readonly documents.TextDocument[] {
2017-11-10 01:01:33 +01:00
return this.host.outputs;
}
public get traces(): readonly string[] {
2017-11-10 01:01:33 +01:00
return this.host.traces;
}
public get emitSkipped(): boolean {
return this.result && this.result.emitSkipped || false;
}
public get singleFile(): boolean {
return !!this.options.outFile || !!this.options.out;
}
public get commonSourceDirectory(): string {
const common = this.program && this.program.getCommonSourceDirectory() || "";
return common && vpath.combine(this.vfs.cwd(), common);
2017-11-10 01:01:33 +01:00
}
public getInputsAndOutputs(path: string): CompilationOutput | undefined {
return this._inputsAndOutputs.get(vpath.resolve(this.vfs.cwd(), path));
2017-11-10 01:01:33 +01:00
}
public getInputs(path: string): readonly documents.TextDocument[] | undefined {
2017-11-10 01:01:33 +01:00
const outputs = this.getInputsAndOutputs(path);
2017-12-21 20:20:44 +01:00
return outputs && outputs.inputs;
2017-11-10 01:01:33 +01:00
}
public getOutput(path: string, kind: "js" | "dts" | "map"): documents.TextDocument | undefined {
2017-11-10 01:01:33 +01:00
const outputs = this.getInputsAndOutputs(path);
return outputs && outputs[kind];
}
2017-11-10 22:52:12 +01:00
public getSourceMapRecord(): string | undefined {
2019-01-11 23:24:49 +01:00
const maps = this.result!.sourceMaps;
if (maps && maps.length > 0) {
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()));
2017-11-10 22:52:12 +01:00
}
}
public getSourceMap(path: string): documents.SourceMap | undefined {
2018-04-20 01:47:07 +02:00
if (this.options.noEmit || vpath.isDeclaration(path)) return undefined;
2017-11-10 01:01:33 +01:00
if (this.options.inlineSourceMap) {
const document = this.getOutput(path, "js");
return document && documents.SourceMap.fromSource(document.text);
2017-11-10 01:01:33 +01:00
}
if (this.options.sourceMap) {
const document = this.getOutput(path, "map");
return document && new documents.SourceMap(document.file, document.text);
2017-11-10 01:01:33 +01:00
}
}
2017-12-21 20:20:44 +01:00
public getOutputPath(path: string, ext: string): string {
if (this.options.outFile || this.options.out) {
path = vpath.resolve(this.vfs.cwd(), this.options.outFile || this.options.out);
2017-12-21 20:20:44 +01:00
}
else {
path = vpath.resolve(this.vfs.cwd(), path);
const outDir = ext === ".d.ts" || ext === ".json.d.ts" || ext === ".d.mts" || ext === ".d.cts" ? this.options.declarationDir || this.options.outDir : this.options.outDir;
2017-12-21 20:20:44 +01:00
if (outDir) {
const common = this.commonSourceDirectory;
if (common) {
path = vpath.relative(common, path, this.vfs.ignoreCase);
path = vpath.combine(vpath.resolve(this.vfs.cwd(), this.options.outDir), path);
2017-12-21 20:20:44 +01:00
}
}
2017-11-10 01:01:33 +01:00
}
2017-12-21 20:20:44 +01:00
return vpath.changeExtension(path, ext);
2017-11-10 01:01:33 +01:00
}
Allow `allowJs` and `declaration` to be used together (#32372) * Allow allowJs and declaration to be used together This intorduces a new symbol-based declaration emitter - currently this is only used for JSON and JavaScript, as the output is likely worse than what the other declaration emitter is capable of. In addition, it is still incomplete - it does not yet support serializaing namespaces. * Add tests for various import/export forms, add notes on export as namespace and fix export * from * Tests & fixes for computed names * Add test with current @enum tag behavior * fix declaration emit for jsdoc @enum tags * Small adjustments to base class serialization to fix bugs in it * Guard against type/type parameter confusion when using typeParameterToName a bit * Integrate feedback from PR * Fix issue with export= declarations visibility calculation and type declaration emit that impacted all forms of declaration emit * Only make one merged getCommonJsExportEquals symbol for a symbol * Support preserving type reference directives in js declarations * Skip declare mdoifiers for namespace members in ambient contexts * FAKE ALIASES AND NAMESPACES EVERYWHERE * Dont do namespace sugar when type members contain keyword names * Fix json source file export modifier under new output * Such clean nested aliasing, very wow * Fix lint * Add visibility errors, reuse type nodes where possible * Suppoer having correctly named import types in bundled js declaration emit & adjust binding to allow namespaces with aliases to merge when the aliases look to be type-only * Better support for module.exports = class expression * Fix discovered crash bug * Allow export assigned class expressions to be reachable symbols from external declarations * Add missing semicolon * Support @enum tag post-merge * preserve comments on signatures and declarations where possible * Basic support for js classy functions * Add example we should do better with * Prototype assignments make things a bit wonky, but the example from the PR seems OK * Make a ton of changes to support the new way js classes are bound * Remove some old comments, fix import and export default names * Fix bug in object define handling and add tests for object define property declaration emit * Fix organization nits from PR comments * Preserve comments from jsdoc declarations on properties and js declaration type aliases * Merge export declarations with identical specifiers * Remove completed TODO comment * Split lint * Remove now-unused function * PR feedback * Add some project references tests, remove some checks from project refs codepaths that are now invalid * Update project references tests again * Merge and update project references tests * Rename case * Update test to include declaration output * Remove yet another project refernces redirect extension check * Update comment * Add additional import ref to test * Add shorthand prop to test * Fix comment text * Extract var to temp * Simplify function and add whitespace * Update project refs test to use incremental edit entry * Stylistic refactors in the symbol serializer * Another round of PR feedback, mostly style, small bugfix with constructors, and test showing bug in export assigned class expression name shadowing * Use x instead of index
2019-09-26 23:27:16 +02:00
public getNumberOfJsFiles(includeJson: boolean) {
if (includeJson) {
return this.js.size;
}
else {
let count = this.js.size;
this.js.forEach(document => {
if (ts.fileExtensionIs(document.file, ts.Extension.Json)) {
count--;
}
});
return count;
}
}
2017-11-10 01:01:33 +01:00
}
2018-04-19 20:30:03 +02:00
export function compileFiles(host: fakes.CompilerHost, rootFiles: string[] | undefined, compilerOptions: ts.CompilerOptions): CompilationResult {
2017-11-10 01:01:33 +01:00
if (compilerOptions.project || !rootFiles || rootFiles.length === 0) {
const project = readProject(host.parseConfigHost, compilerOptions.project, compilerOptions);
if (project) {
if (project.errors && project.errors.length > 0) {
return new CompilationResult(host, compilerOptions, /*program*/ undefined, /*result*/ undefined, project.errors);
}
if (project.config) {
rootFiles = project.config.fileNames;
compilerOptions = project.config.options;
}
}
delete compilerOptions.project;
}
2017-11-10 22:52:12 +01:00
// establish defaults (aligns with old harness)
if (compilerOptions.target === undefined && compilerOptions.module !== ts.ModuleKind.Node12 && compilerOptions.module !== ts.ModuleKind.NodeNext) compilerOptions.target = ts.ScriptTarget.ES3;
2017-11-10 01:01:33 +01:00
if (compilerOptions.newLine === undefined) compilerOptions.newLine = ts.NewLineKind.CarriageReturnLineFeed;
if (compilerOptions.skipDefaultLibCheck === undefined) compilerOptions.skipDefaultLibCheck = true;
if (compilerOptions.noErrorTruncation === undefined) compilerOptions.noErrorTruncation = true;
// pre-emit/post-emit error comparison requires declaration emit twice, which can be slow. If it's unlikely to flag any error consistency issues
// and if the test is running `skipLibCheck` - an indicator that we want the tets to run quickly - skip the before/after error comparison, too
const skipErrorComparison = ts.length(rootFiles) >= 100 || (!!compilerOptions.skipLibCheck && !!compilerOptions.declaration);
const preProgram = !skipErrorComparison ? ts.createProgram(rootFiles || [], { ...compilerOptions, configFile: compilerOptions.configFile, traceResolution: false }, host) : undefined;
const preErrors = preProgram && ts.getPreEmitDiagnostics(preProgram);
2017-11-10 01:01:33 +01:00
const program = ts.createProgram(rootFiles || [], compilerOptions, host);
const emitResult = program.emit();
const postErrors = ts.getPreEmitDiagnostics(program);
const longerErrors = ts.length(preErrors) > postErrors.length ? preErrors : postErrors;
const shorterErrors = longerErrors === preErrors ? postErrors : preErrors;
const errors = preErrors && (preErrors.length !== postErrors.length) ? [...shorterErrors!,
ts.addRelatedInfo(
ts.createCompilerDiagnostic({
category: ts.DiagnosticCategory.Error,
code: -1,
key: "-1",
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!`
}),
ts.createCompilerDiagnostic({
category: ts.DiagnosticCategory.Error,
code: -1,
key: "-1",
message: `The excess diagnostics are:`
}),
...ts.filter(longerErrors!, p => !ts.some(shorterErrors, p2 => ts.compareDiagnostics(p, p2) === ts.Comparison.EqualTo))
)
] : postErrors;
2017-11-10 01:01:33 +01:00
return new CompilationResult(host, compilerOptions, program, emitResult, errors);
}
2018-05-04 00:35:54 +02:00
}