From f0b7e08d2ca2d6380bea31f36d32f28d3b905a84 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 9 May 2019 13:13:50 -0700 Subject: [PATCH] Move towards BuildInvalidatedProject api where one can query program and perform its operations --- src/compiler/tsbuild.ts | 490 ++++++++++++++++++++++++++-------------- src/compiler/watch.ts | 40 +++- src/tsc/tsc.ts | 2 +- 3 files changed, 359 insertions(+), 173 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 05e6d0d2790..5498c710983 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -675,17 +675,44 @@ namespace ts { updateOutputFileStatmps(): void; } - interface BuildInvalidedProject extends InvalidatedProjectBase { + interface BuildInvalidedProject extends InvalidatedProjectBase { readonly kind: InvalidatedProjectKind.Build; - build(cancellationToken?: CancellationToken): BuildResultFlags; + /* + * Emitting with this builder program without the api provided for this project + * can result in build system going into invalid state as files written reflect the state of the project + */ + getBuilderProgram(): T | undefined; + getProgram(): Program | undefined; + getCompilerOptions(): CompilerOptions; + getSourceFile(fileName: string): SourceFile | undefined; + getSourceFiles(): ReadonlyArray; + getOptionsDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + getGlobalDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + getConfigFileParsingDiagnostics(): ReadonlyArray; + getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + getAllDependencies(sourceFile: SourceFile): ReadonlyArray; + getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult>; + /* + * Calling emit directly with targetSourceFile and emitOnlyDtsFiles set to true is not advised since + * emit in build system is responsible in updating status of the project + * If called with targetSourceFile and emitOnlyDtsFiles set to true, the emit just passes to underlying builder and + * wont reflect the status of file as being emitted in the builder + * (if that emit of that source file is required it would be emitted again when making sure invalidated project is completed) + * This emit is not considered actual emit (and hence uptodate status is not reflected if + */ + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult | undefined; + // TODO(shkamat):: investigate later if we can emit even when there are declaration diagnostics + // emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): AffectedFileResult; + getCurrentDirectory(): string; } - interface UpdateBundleProject extends InvalidatedProjectBase { + interface UpdateBundleProject extends InvalidatedProjectBase { readonly kind: InvalidatedProjectKind.UpdateBundle; - updateBundle(): BuildResultFlags | BuildInvalidedProject; + updateBundle(): BuildResultFlags | BuildInvalidedProject; } - type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; + type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; function createUpdateOutputFileStampsProject(state: SolutionBuilderState, project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, config: ParsedCommandLine): UpdateOutputFileStampsProject { let updateOutputFileStampsPending = true; @@ -706,42 +733,318 @@ namespace ts { }; } - function createBuildInvalidedProject( - state: SolutionBuilderState, + function createBuildInvalidedProject( + state: SolutionBuilderState, project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, projectIndex: number, config: ParsedCommandLine, buildOrder: readonly ResolvedConfigFileName[] - ): BuildInvalidedProject { - let buildPending = true; + ): BuildInvalidedProject { + enum Step { + CreateProgram, + SyntaxDiagnostics, + SemanticDiagnostics, + Emit, + QueueReferencingProjects, + Done + } + + let step = Step.CreateProgram; + let program: T | undefined; + let buildResult: BuildResultFlags | undefined; + return { kind: InvalidatedProjectKind.Build, project, projectPath, - build, + getBuilderProgram: () => withProgramOrUndefined(identity), + getProgram: () => + withProgramOrUndefined( + program => program.getProgramOrUndefined() + ), + getCompilerOptions: () => config.options, + getSourceFile: fileName => + withProgramOrUndefined( + program => program.getSourceFile(fileName) + ), + getSourceFiles: () => + withProgramOrEmptyArray( + program => program.getSourceFiles() + ), + getOptionsDiagnostics: cancellationToken => + withProgramOrEmptyArray( + program => program.getOptionsDiagnostics(cancellationToken) + ), + getGlobalDiagnostics: cancellationToken => + withProgramOrEmptyArray( + program => program.getGlobalDiagnostics(cancellationToken) + ), + getConfigFileParsingDiagnostics: () => + withProgramOrEmptyArray( + program => program.getConfigFileParsingDiagnostics() + ), + getSyntacticDiagnostics: (sourceFile, cancellationToken) => + withProgramOrEmptyArray( + program => program.getSyntacticDiagnostics(sourceFile, cancellationToken) + ), + getAllDependencies: sourceFile => + withProgramOrEmptyArray( + program => program.getAllDependencies(sourceFile) + ), + getSemanticDiagnostics: (sourceFile, cancellationToken) => + withProgramOrEmptyArray( + program => program.getSemanticDiagnostics(sourceFile, cancellationToken) + ), + getSemanticDiagnosticsOfNextAffectedFile: (cancellationToken, ignoreSourceFile) => + withProgramOrUndefined( + program => + ((program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile) && + (program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile(cancellationToken, ignoreSourceFile) + ), + emit: (targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers) => { + if (targetSourceFile || emitOnlyDtsFiles) { + return withProgramOrUndefined( + program => program.emit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers) + ); + } + executeSteps(Step.SemanticDiagnostics, cancellationToken); + if (step !== Step.Emit) return undefined; + return emit(writeFile, cancellationToken, customTransformers); + }, + getCurrentDirectory: () => state.currentDirectory, done: cancellationToken => { - if (buildPending) build(cancellationToken); + executeSteps(Step.Done, cancellationToken); state.projectPendingBuild.delete(projectPath); } }; - function build(cancellationToken?: CancellationToken) { - const buildResult = buildSingleProject(state, project, projectPath, config, cancellationToken); - queueReferencingProjects(state, project, projectPath, projectIndex, config, buildOrder, buildResult); - buildPending = false; - return buildResult; + function withProgramOrUndefined(action: (program: T) => U | undefined): U | undefined { + executeSteps(Step.CreateProgram); + return program && action(program); + } + + function withProgramOrEmptyArray(action: (program: T) => ReadonlyArray): ReadonlyArray { + return withProgramOrUndefined(action) || emptyArray; + } + + function createProgram() { + Debug.assert(program === undefined); + + if (state.options.dry) { + reportStatus(state, Diagnostics.A_non_dry_build_would_build_project_0, project); + buildResult = BuildResultFlags.Success; + step = Step.QueueReferencingProjects; + return; + } + + if (state.options.verbose) reportStatus(state, Diagnostics.Building_project_0, project); + + if (config.fileNames.length === 0) { + reportAndStoreErrors(state, projectPath, config.errors); + // Nothing to build - must be a solution file, basically + buildResult = BuildResultFlags.None; + step = Step.QueueReferencingProjects; + return; + } + + const { host, compilerHost } = state; + state.projectCompilerOptions = config.options; + // Update module resolution cache if needed + updateModuleResolutionCache(state, project, config); + + // Create program + program = host.createProgram( + config.fileNames, + config.options, + compilerHost, + getOldProgram(state, projectPath, config), + config.errors, + config.projectReferences + ); + step++; + } + + function handleDiagnostics(diagnostics: ReadonlyArray, errorFlags: BuildResultFlags, errorType: string) { + if (diagnostics.length) { + buildResult = buildErrors( + state, + projectPath, + program, + diagnostics, + errorFlags, + errorType + ); + step = Step.QueueReferencingProjects; + } + else { + step++; + } + } + + function getSyntaxDiagnostics(cancellationToken?: CancellationToken) { + Debug.assertDefined(program); + handleDiagnostics( + [ + ...program!.getConfigFileParsingDiagnostics(), + ...program!.getOptionsDiagnostics(cancellationToken), + ...program!.getGlobalDiagnostics(cancellationToken), + ...program!.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken) + ], + BuildResultFlags.SyntaxErrors, + "Syntactic" + ); + } + + function getSemanticDiagnostics(cancellationToken?: CancellationToken) { + handleDiagnostics( + Debug.assertDefined(program).getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken), + BuildResultFlags.TypeErrors, + "Semantic" + ); + } + + function emit(writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitResult { + Debug.assertDefined(program); + Debug.assert(step === Step.Emit); + // Before emitting lets backup state, so we can revert it back if there are declaration errors to handle emit and declaration errors correctly + program!.backupState(); + let declDiagnostics: Diagnostic[] | undefined; + const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d); + const outputFiles: OutputFile[] = []; + const { emitResult } = emitFilesAndReportErrors( + program!, + reportDeclarationDiagnostics, + /*writeFileName*/ undefined, + /*reportSummary*/ undefined, + (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }), + cancellationToken, + /*emitOnlyDts*/ false, + customTransformers + ); + // Don't emit .d.ts if there are decl file errors + if (declDiagnostics) { + program!.restoreState(); + buildResult = buildErrors( + state, + projectPath, + program, + declDiagnostics, + BuildResultFlags.DeclarationEmitErrors, + "Declaration file" + ); + step = Step.QueueReferencingProjects; + return { + emitSkipped: true, + diagnostics: emitResult.diagnostics + }; + } + + // Actual Emit + const { host, compilerHost, diagnostics, projectStatus } = state; + let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; + let newestDeclarationFileContentChangedTime = minimumDate; + let anyDtsChanged = false; + const emitterDiagnostics = createDiagnosticCollection(); + const emittedOutputs = createMap() as FileMap; + outputFiles.forEach(({ name, text, writeByteOrderMark }) => { + let priorChangeTime: Date | undefined; + if (!anyDtsChanged && isDeclarationFile(name)) { + // Check for unchanged .d.ts files + if (host.fileExists(name) && state.readFileWithCache(name) === text) { + priorChangeTime = host.getModifiedTime(name); + } + else { + resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; + anyDtsChanged = true; + } + } + + emittedOutputs.set(toPath(state, name), name); + writeFile(writeFileCallback ? { writeFile: writeFileCallback } : compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); + if (priorChangeTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); + } + }); + + const emitDiagnostics = emitterDiagnostics.getDiagnostics(); + if (emitDiagnostics.length) { + buildResult = buildErrors( + state, + projectPath, + program, + emitDiagnostics, + BuildResultFlags.EmitErrors, + "Emit" + ); + step = Step.QueueReferencingProjects; + return emitResult; + } + + if (state.writeFileName) { + emittedOutputs.forEach(name => listEmittedFile(state, config, name)); + listFiles(program!, state.writeFileName); + } + + // Update time stamps for rest of the outputs + newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + diagnostics.delete(projectPath); + projectStatus.set(projectPath, { + type: UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime, + oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(config, !host.useCaseSensitiveFileNames()) + }); + afterProgramCreate(state, projectPath, program!); + state.projectCompilerOptions = state.baseCompilerOptions; + step++; + buildResult = resultFlags; + return emitResult; + } + + function executeSteps(till: Step, cancellationToken?: CancellationToken) { + while (step <= till && step < Step.Done) { + const currentStep = step; + switch (step) { + case Step.CreateProgram: + createProgram(); + break; + + case Step.SyntaxDiagnostics: + getSyntaxDiagnostics(cancellationToken); + break; + + case Step.SemanticDiagnostics: + getSemanticDiagnostics(cancellationToken); + break; + + case Step.Emit: + emit(/*writeFileCallback*/ undefined, cancellationToken); + break; + + case Step.QueueReferencingProjects: + queueReferencingProjects(state, project, projectPath, projectIndex, config, buildOrder, Debug.assertDefined(buildResult)); + step++; + break; + + // Should never be done + case Step.Done: + default: + assertType(step); + + } + Debug.assert(step > currentStep); + } } } - function createUpdateBundleProject( - state: SolutionBuilderState, + function createUpdateBundleProject( + state: SolutionBuilderState, project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, projectIndex: number, config: ParsedCommandLine, buildOrder: readonly ResolvedConfigFileName[] - ): UpdateBundleProject { + ): UpdateBundleProject { let updatePending = true; return { kind: InvalidatedProjectKind.UpdateBundle, @@ -777,7 +1080,7 @@ namespace ts { !isIncrementalCompilation(config.options); } - function getNextInvalidatedProject(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { + function getNextInvalidatedProject(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { if (!state.projectPendingBuild.size) return undefined; const { options, projectPendingBuild } = state; @@ -925,153 +1228,6 @@ namespace ts { moduleResolutionCache.moduleNameToDirectoryMap.setOwnOptions(config.options); } - function buildSingleProject( - state: SolutionBuilderState, - proj: ResolvedConfigFileName, - resolvedPath: ResolvedConfigFilePath, - config: ParsedCommandLine, - cancellationToken: CancellationToken | undefined - ): BuildResultFlags { - if (state.options.dry) { - reportStatus(state, Diagnostics.A_non_dry_build_would_build_project_0, proj); - return BuildResultFlags.Success; - } - - if (state.options.verbose) reportStatus(state, Diagnostics.Building_project_0, proj); - - if (config.fileNames.length === 0) { - reportAndStoreErrors(state, resolvedPath, config.errors); - // Nothing to build - must be a solution file, basically - return BuildResultFlags.None; - } - - const { host, projectStatus, diagnostics, compilerHost } = state; - state.projectCompilerOptions = config.options; - // Update module resolution cache if needed - updateModuleResolutionCache(state, proj, config); - - // Create program - const program = host.createProgram( - config.fileNames, - config.options, - compilerHost, - getOldProgram(state, resolvedPath, config), - config.errors, - config.projectReferences - ); - - // Don't emit anything in the presence of syntactic errors or options diagnostics - const syntaxDiagnostics = [ - ...program.getConfigFileParsingDiagnostics(), - ...program.getOptionsDiagnostics(cancellationToken), - ...program.getGlobalDiagnostics(cancellationToken), - ...program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken)]; - if (syntaxDiagnostics.length) { - return buildErrors( - state, - resolvedPath, - program, - syntaxDiagnostics, - BuildResultFlags.SyntaxErrors, - "Syntactic" - ); - } - - // Same as above but now for semantic diagnostics - const semanticDiagnostics = program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken); - if (semanticDiagnostics.length) { - return buildErrors( - state, - resolvedPath, - program, - semanticDiagnostics, - BuildResultFlags.TypeErrors, - "Semantic" - ); - } - - // Before emitting lets backup state, so we can revert it back if there are declaration errors to handle emit and declaration errors correctly - program.backupState(); - let declDiagnostics: Diagnostic[] | undefined; - const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d); - const outputFiles: OutputFile[] = []; - emitFilesAndReportErrors( - program, - reportDeclarationDiagnostics, - /*writeFileName*/ undefined, - /*reportSummary*/ undefined, - (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }), - cancellationToken - ); - // Don't emit .d.ts if there are decl file errors - if (declDiagnostics) { - program.restoreState(); - return buildErrors( - state, - resolvedPath, - program, - declDiagnostics, - BuildResultFlags.DeclarationEmitErrors, - "Declaration file" - ); - } - - // Actual Emit - let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; - let newestDeclarationFileContentChangedTime = minimumDate; - let anyDtsChanged = false; - const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createMap() as FileMap; - outputFiles.forEach(({ name, text, writeByteOrderMark }) => { - let priorChangeTime: Date | undefined; - if (!anyDtsChanged && isDeclarationFile(name)) { - // Check for unchanged .d.ts files - if (host.fileExists(name) && state.readFileWithCache(name) === text) { - priorChangeTime = host.getModifiedTime(name); - } - else { - resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; - anyDtsChanged = true; - } - } - - emittedOutputs.set(toPath(state, name), name); - writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); - if (priorChangeTime !== undefined) { - newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); - } - }); - - const emitDiagnostics = emitterDiagnostics.getDiagnostics(); - if (emitDiagnostics.length) { - return buildErrors( - state, - resolvedPath, - program, - emitDiagnostics, - BuildResultFlags.EmitErrors, - "Emit" - ); - } - - if (state.writeFileName) { - emittedOutputs.forEach(name => listEmittedFile(state, config, name)); - listFiles(program, state.writeFileName); - } - - // Update time stamps for rest of the outputs - newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); - diagnostics.delete(resolvedPath); - projectStatus.set(resolvedPath, { - type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime, - oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(config, !host.useCaseSensitiveFileNames()) - }); - afterProgramCreate(state, resolvedPath, program); - state.projectCompilerOptions = state.baseCompilerOptions; - return resultFlags; - } - function updateBundle( state: SolutionBuilderState, proj: ResolvedConfigFileName, diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 4f974add57d..9378478664f 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -116,7 +116,7 @@ namespace ts { getGlobalDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; getConfigFileParsingDiagnostics(): ReadonlyArray; - emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult; + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult; } export function listFiles(program: ProgramToEmitFilesAndReportErrors, writeFileName: (s: string) => void) { @@ -136,7 +136,9 @@ namespace ts { writeFileName?: (s: string) => void, reportSummary?: ReportEmitErrorSummary, writeFile?: WriteFileCallback, - cancellationToken?: CancellationToken + cancellationToken?: CancellationToken, + emitOnlyDtsFiles?: boolean, + customTransformers?: CustomTransformers ) { // First get and report any syntactic errors. const diagnostics = program.getConfigFileParsingDiagnostics().slice(); @@ -155,7 +157,8 @@ namespace ts { } // Emit and report any errors we ran into. - const { emittedFiles, emitSkipped, diagnostics: emitDiagnostics } = program.emit(/*targetSourceFile*/ undefined, writeFile, cancellationToken); + const emitResult = program.emit(/*targetSourceFile*/ undefined, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); + const { emittedFiles, diagnostics: emitDiagnostics } = emitResult; addRange(diagnostics, emitDiagnostics); sortAndDeduplicateDiagnostics(diagnostics).forEach(reportDiagnostic); @@ -172,7 +175,34 @@ namespace ts { reportSummary(getErrorCountForSummary(diagnostics)); } - if (emitSkipped && diagnostics.length > 0) { + return { + emitResult, + diagnostics, + }; + } + + export function emitFilesAndReportErrorsAndGetExitStatus( + program: ProgramToEmitFilesAndReportErrors, + reportDiagnostic: DiagnosticReporter, + writeFileName?: (s: string) => void, + reportSummary?: ReportEmitErrorSummary, + writeFile?: WriteFileCallback, + cancellationToken?: CancellationToken, + emitOnlyDtsFiles?: boolean, + customTransformers?: CustomTransformers + ) { + const { emitResult, diagnostics } = emitFilesAndReportErrors( + program, + reportDiagnostic, + writeFileName, + reportSummary, + writeFile, + cancellationToken, + emitOnlyDtsFiles, + customTransformers + ); + + if (emitResult.emitSkipped && diagnostics.length > 0) { // If the emitter didn't emit anything, then pass that value along. return ExitStatus.DiagnosticsPresent_OutputsSkipped; } @@ -395,7 +425,7 @@ namespace ts { const system = input.system || sys; const host = input.host || (input.host = createIncrementalCompilerHost(input.options, system)); const builderProgram = createIncrementalProgram(input); - const exitStatus = emitFilesAndReportErrors( + const exitStatus = emitFilesAndReportErrorsAndGetExitStatus( builderProgram, input.reportDiagnostic || createDiagnosticReporter(system), s => host.trace && host.trace(s), diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index 47814a1f68b..6a340109238 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -246,7 +246,7 @@ namespace ts { configFileParsingDiagnostics }; const program = createProgram(programOptions); - const exitStatus = emitFilesAndReportErrors( + const exitStatus = emitFilesAndReportErrorsAndGetExitStatus( program, reportDiagnostic, s => sys.write(s + sys.newLine),