Introduce helper gatherDiagnostics in tsc_wrapped
PiperOrigin-RevId: 221529157
diff --git a/internal/tsc_wrapped/tsc_wrapped.ts b/internal/tsc_wrapped/tsc_wrapped.ts
index 747be13..d98ec95 100644
--- a/internal/tsc_wrapped/tsc_wrapped.ts
+++ b/internal/tsc_wrapped/tsc_wrapped.ts
@@ -6,12 +6,15 @@
import {CachedFileLoader, FileLoader, ProgramAndFileCache, UncachedFileLoader} from './cache';
import {CompilerHost} from './compiler_host';
-import * as diagnostics from './diagnostics';
-import {wrap} from './perf_trace';
+import * as bazelDiagnostics from './diagnostics';
+import * as perfTrace from './perf_trace';
import {PLUGIN as strictDepsPlugin} from './strict_deps';
-import {parseTsconfig, resolveNormalizedPath} from './tsconfig';
+import {BazelOptions, parseTsconfig, resolveNormalizedPath} from './tsconfig';
import {debug, log, runAsWorker, runWorkerLoop} from './worker';
+/**
+ * Top-level entry point for tsc_wrapped.
+ */
export function main(args: string[]) {
if (runAsWorker(args)) {
log('Starting TypeScript compiler persistent worker...');
@@ -28,76 +31,21 @@
return 0;
}
-// The one ProgramAndFileCache instance used in this process.
+/** The one ProgramAndFileCache instance used in this process. */
const cache = new ProgramAndFileCache(debug);
+function isCompilationTarget(
+ bazelOpts: BazelOptions, sf: ts.SourceFile): boolean {
+ return (bazelOpts.compilationTargetSrc.indexOf(sf.fileName) !== -1);
+}
+
/**
- * Runs a single build, returning false on failure. This is potentially called
- * multiple times (once per bazel request) when running as a bazel worker.
- * Any encountered errors are written to stderr.
+ * Gather diagnostics from TypeScript's type-checker as well as other plugins we
+ * install such as strict dependency checking.
*/
-function runOneBuild(
- args: string[], inputs?: {[path: string]: string}): boolean {
- if (args.length !== 1) {
- console.error('Expected one argument: path to tsconfig.json');
- return false;
- }
- // Strip leading at-signs, used in build_defs.bzl to indicate a params file
- const tsconfigFile = args[0].replace(/^@+/, '');
-
- const [parsed, errors, {target}] = parseTsconfig(tsconfigFile);
- if (errors) {
- console.error(diagnostics.format(target, errors));
- return false;
- }
- if (!parsed) {
- throw new Error(
- 'Impossible state: if parseTsconfig returns no errors, then parsed should be non-null');
- }
- const {options, bazelOpts, files, disabledTsetseRules} = parsed;
-
- // Reset cache stats.
- cache.resetStats();
- cache.traceStats();
- if (bazelOpts.maxCacheSizeMb !== undefined) {
- const maxCacheSizeBytes = bazelOpts.maxCacheSizeMb * 1 << 20;
- cache.setMaxCacheSize(maxCacheSizeBytes);
- } else {
- cache.resetMaxCacheSize();
- }
-
- let fileLoader: FileLoader;
- const allowActionInputReads = true;
-
- if (inputs) {
- fileLoader = new CachedFileLoader(cache);
- // Resolve the inputs to absolute paths to match TypeScript internals
- const resolvedInputs = new Map<string, string>();
- for (const key of Object.keys(inputs)) {
- resolvedInputs.set(resolveNormalizedPath(key), inputs[key]);
- }
- cache.updateCache(resolvedInputs);
- } else {
- fileLoader = new UncachedFileLoader();
- }
-
- const compilerHostDelegate =
- ts.createCompilerHost({target: ts.ScriptTarget.ES5});
-
- const compilerHost = new CompilerHost(
- files, options, bazelOpts, compilerHostDelegate, fileLoader,
- allowActionInputReads);
-
- let oldProgram = cache.getProgram(bazelOpts.target);
- let program = ts.createProgram(files, options, compilerHost, oldProgram);
- cache.putProgram(bazelOpts.target, program);
-
- cache.traceStats();
-
- function isCompilationTarget(sf: ts.SourceFile): boolean {
- return (bazelOpts.compilationTargetSrc.indexOf(sf.fileName) !== -1);
- }
- let diags: ts.Diagnostic[] = [];
+export function gatherDiagnostics(
+ options: ts.CompilerOptions, bazelOpts: BazelOptions, program: ts.Program,
+ disabledTsetseRules: string[]): ts.Diagnostic[] {
// Install extra diagnostic plugins
if (!bazelOpts.disableStrictDeps) {
const ignoredFilesPrefixes: string[] = [];
@@ -119,39 +67,144 @@
ignoredFilesPrefixes,
});
}
- program = tsetsePlugin.wrap(program, disabledTsetseRules);
-
- // These checks mirror ts.getPreEmitDiagnostics, with the important
- // exception that if you call program.getDeclarationDiagnostics() it somehow
- // corrupts the emit.
- wrap(`global diagnostics`, () => {
- diags.push(...program.getOptionsDiagnostics());
- diags.push(...program.getGlobalDiagnostics());
- });
- let sourceFilesToCheck: ReadonlyArray<ts.SourceFile>;
- if (bazelOpts.typeCheckDependencies) {
- sourceFilesToCheck = program.getSourceFiles();
- } else {
- sourceFilesToCheck = program.getSourceFiles().filter(isCompilationTarget);
+ if (!bazelOpts.isJsTranspilation) {
+ program = tsetsePlugin.wrap(program, disabledTsetseRules);
}
- for (const sf of sourceFilesToCheck) {
- wrap(`check ${sf.fileName}`, () => {
- diags.push(...program.getSyntacticDiagnostics(sf));
- diags.push(...program.getSemanticDiagnostics(sf));
+
+ // TODO(alexeagle): support plugins registered by config
+
+ const diagnostics: ts.Diagnostic[] = [];
+ perfTrace.wrap('type checking', () => {
+ // These checks mirror ts.getPreEmitDiagnostics, with the important
+ // exception of avoiding b/30708240, which is that if you call
+ // program.getDeclarationDiagnostics() it somehow corrupts the emit.
+ perfTrace.wrap(`global diagnostics`, () => {
+ diagnostics.push(...program.getOptionsDiagnostics());
+ diagnostics.push(...program.getGlobalDiagnostics());
});
- }
+ let sourceFilesToCheck: ReadonlyArray<ts.SourceFile>;
+ if (bazelOpts.typeCheckDependencies) {
+ sourceFilesToCheck = program.getSourceFiles();
+ } else {
+ sourceFilesToCheck = program.getSourceFiles().filter(
+ f => isCompilationTarget(bazelOpts, f));
+ }
+ for (const sf of sourceFilesToCheck) {
+ perfTrace.wrap(`check ${sf.fileName}`, () => {
+ diagnostics.push(...program.getSyntacticDiagnostics(sf));
+ diagnostics.push(...program.getSemanticDiagnostics(sf));
+ });
+ perfTrace.snapshotMemoryUsage();
+ }
+ });
- // If there are any TypeScript type errors abort now, so the error
- // messages refer to the original source. After any subsequent passes
- // (decorator downleveling or tsickle) we do not type check.
- diags = diagnostics.filterExpected(bazelOpts, diags);
- if (diags.length > 0) {
- console.error(diagnostics.format(bazelOpts.target, diags));
+ return diagnostics;
+}
+
+/**
+ * Runs a single build, returning false on failure. This is potentially called
+ * multiple times (once per bazel request) when running as a bazel worker.
+ * Any encountered errors are written to stderr.
+ */
+function runOneBuild(
+ args: string[], inputs?: {[path: string]: string}): boolean {
+ if (args.length !== 1) {
+ console.error('Expected one argument: path to tsconfig.json');
return false;
}
- const toEmit = program.getSourceFiles().filter(isCompilationTarget);
- const emitResults: ts.EmitResult[] = [];
+ perfTrace.snapshotMemoryUsage();
+
+ // Strip leading at-signs, used in build_defs.bzl to indicate a params file
+ const tsconfigFile = args[0].replace(/^@+/, '');
+ const [parsed, errors, {target}] = parseTsconfig(tsconfigFile);
+ if (errors) {
+ console.error(bazelDiagnostics.format(target, errors));
+ return false;
+ }
+ if (!parsed) {
+ throw new Error(
+ 'Impossible state: if parseTsconfig returns no errors, then parsed should be non-null');
+ }
+ const {options, bazelOpts, files, disabledTsetseRules} = parsed;
+
+ if (bazelOpts.maxCacheSizeMb !== undefined) {
+ const maxCacheSizeBytes = bazelOpts.maxCacheSizeMb * (1 << 20);
+ cache.setMaxCacheSize(maxCacheSizeBytes);
+ } else {
+ cache.resetMaxCacheSize();
+ }
+
+ let fileLoader: FileLoader;
+ if (inputs) {
+ fileLoader = new CachedFileLoader(cache);
+ // Resolve the inputs to absolute paths to match TypeScript internals
+ const resolvedInputs = new Map<string, string>();
+ for (const key of Object.keys(inputs)) {
+ resolvedInputs.set(resolveNormalizedPath(key), inputs[key]);
+ }
+ cache.updateCache(resolvedInputs);
+ } else {
+ fileLoader = new UncachedFileLoader();
+ }
+
+ const perfTracePath = bazelOpts.perfTracePath;
+ if (!perfTracePath) {
+ return runFromOptions(
+ fileLoader, options, bazelOpts, files, disabledTsetseRules);
+ }
+
+ log('Writing trace to', perfTracePath);
+ const success = perfTrace.wrap(
+ 'runOneBuild',
+ () => runFromOptions(
+ fileLoader, options, bazelOpts, files, disabledTsetseRules));
+ if (!success) return false;
+ // Force a garbage collection pass. This keeps our memory usage
+ // consistent across multiple compilations, and allows the file
+ // cache to use the current memory usage as a guideline for expiring
+ // data. Note: this is intentionally not within runFromOptions(), as
+ // we want to gc only after all its locals have gone out of scope.
+ global.gc();
+
+ perfTrace.snapshotMemoryUsage();
+ perfTrace.write(perfTracePath);
+
+ return true;
+}
+
+// We only allow our own code to use the expected_diagnostics attribute
+const expectDiagnosticsWhitelist: string[] = [
+];
+
+function runFromOptions(
+ fileLoader: FileLoader, options: ts.CompilerOptions,
+ bazelOpts: BazelOptions, files: string[],
+ disabledTsetseRules: string[]): boolean {
+ perfTrace.snapshotMemoryUsage();
+ cache.resetStats();
+ cache.traceStats();
+ const compilerHostDelegate =
+ ts.createCompilerHost({target: ts.ScriptTarget.ES5});
+
+ const allowActionInputReads = true;
+ const compilerHost = new CompilerHost(
+ files, options, bazelOpts, compilerHostDelegate, fileLoader,
+ allowActionInputReads);
+
+
+ const oldProgram = cache.getProgram(bazelOpts.target);
+ const program = perfTrace.wrap(
+ 'createProgram',
+ () => ts.createProgram(
+ compilerHost.inputFiles, options, compilerHost, oldProgram));
+ cache.putProgram(bazelOpts.target, program);
+
+
+ const compilationTargets =
+ program.getSourceFiles().filter(f => isCompilationTarget(bazelOpts, f));
+ const emitResults: ts.EmitResult[] = [];
+ const diagnostics: ts.Diagnostic[] = [];
if (bazelOpts.tsickle) {
// The 'tsickle' import above is only used in type positions, so it won't
// result in a runtime dependency on tsickle.
@@ -167,29 +220,36 @@
'When setting bazelOpts { tsickle: true }, ' +
'you must also add a devDependency on the tsickle npm package');
}
- for (const sf of toEmit) {
+ for (const sf of compilationTargets) {
emitResults.push(optTsickle.emitWithTsickle(
program, compilerHost, compilerHost, options, sf));
}
- diags.push(
+ diagnostics.push(
...optTsickle.mergeEmitResults(emitResults as tsickle.EmitResult[])
.diagnostics);
} else {
- for (const sf of toEmit) {
+ for (const sf of compilationTargets) {
emitResults.push(program.emit(sf));
}
for (const d of emitResults) {
- diags.push(...d.diagnostics);
+ diagnostics.push(...d.diagnostics);
}
}
- if (diags.length > 0) {
- console.error(diagnostics.format(bazelOpts.target, diags));
+ if (diagnostics.length > 0) {
+ console.error(bazelDiagnostics.format(bazelOpts.target, diagnostics));
return false;
}
+
+ cache.printStats();
return true;
}
+
if (require.main === module) {
+ // Do not call process.exit(), as that terminates the binary before
+ // completing pending operations, such as writing to stdout or emitting the
+ // v8 performance log. Rather, set the exit code and fall off the main
+ // thread, which will cause node to terminate cleanly.
process.exitCode = main(process.argv.slice(2));
}