From 745b59f49cacb55c54d11e948c011fbbf8b724f7 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 29 Sep 2017 15:02:11 -0700 Subject: [PATCH] perf(compiler): only emit changed files for incremental compilation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For now, we always create all generated files, but diff them before we pass them to TypeScript. For the user files, we compare the programs and only emit changed TypeScript files. This also adds more diagnostic messages if the `—diagnostics` flag is passed to the command line. --- packages/compiler-cli/src/main.ts | 7 +- packages/compiler-cli/src/ngtools_api2.ts | 11 +- packages/compiler-cli/src/perform_compile.ts | 13 +- packages/compiler-cli/src/perform_watch.ts | 30 +--- packages/compiler-cli/src/transformers/api.ts | 14 +- .../src/transformers/compiler_host.ts | 4 +- .../transformers/node_emitter_transform.ts | 9 +- .../compiler-cli/src/transformers/program.ts | 148 ++++++++++++++--- .../compiler-cli/src/transformers/util.ts | 15 +- .../compiler-cli/test/perform_watch_spec.ts | 6 +- packages/compiler-cli/test/test_support.ts | 5 +- .../test/transformers/compiler_host_spec.ts | 2 +- .../test/transformers/program_spec.ts | 80 +++++++-- packages/compiler/src/aot/generated_file.ts | 17 +- packages/compiler/src/output/output_ast.ts | 157 +++++++++++++++++- 15 files changed, 424 insertions(+), 94 deletions(-) diff --git a/packages/compiler-cli/src/main.ts b/packages/compiler-cli/src/main.ts index 626904b7b8..b34cf29596 100644 --- a/packages/compiler-cli/src/main.ts +++ b/packages/compiler-cli/src/main.ts @@ -18,7 +18,7 @@ import * as api from './transformers/api'; import * as ngc from './transformers/entry_points'; import {GENERATED_FILES} from './transformers/util'; -import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult} from './perform_compile'; +import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult, filterErrorsAndWarnings} from './perform_compile'; import {performWatchCompilation, createPerformWatchHost} from './perform_watch'; import {isSyntaxError} from '@angular/compiler'; @@ -130,8 +130,9 @@ export function readCommandLineAndConfiguration( function reportErrorsAndExit( options: api.CompilerOptions, allDiagnostics: Diagnostics, consoleError: (s: string) => void = console.error): number { - if (allDiagnostics.length) { - consoleError(formatDiagnostics(options, allDiagnostics)); + const errorsAndWarnings = filterErrorsAndWarnings(allDiagnostics); + if (errorsAndWarnings.length) { + consoleError(formatDiagnostics(options, errorsAndWarnings)); } return exitCodeFromResult(allDiagnostics); } diff --git a/packages/compiler-cli/src/ngtools_api2.ts b/packages/compiler-cli/src/ngtools_api2.ts index 9c0ab6584b..d4c1c0ab2f 100644 --- a/packages/compiler-cli/src/ngtools_api2.ts +++ b/packages/compiler-cli/src/ngtools_api2.ts @@ -19,9 +19,11 @@ import {ParseSourceSpan} from '@angular/compiler'; import * as ts from 'typescript'; import {formatDiagnostics as formatDiagnosticsOrig} from './perform_compile'; +import {Program as ProgramOrig} from './transformers/api'; import {createCompilerHost as createCompilerOrig} from './transformers/compiler_host'; import {createProgram as createProgramOrig} from './transformers/program'; + // Interfaces from ./transformers/api; export interface Diagnostic { messageText: string; @@ -92,12 +94,6 @@ export interface TsEmitArguments { export interface TsEmitCallback { (args: TsEmitArguments): ts.EmitResult; } -export interface LibrarySummary { - fileName: string; - text: string; - sourceFile?: ts.SourceFile; -} - export interface Program { getTsProgram(): ts.Program; getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken): ts.Diagnostic[]; @@ -116,7 +112,6 @@ export interface Program { customTransformers?: CustomTransformers, emitCallback?: TsEmitCallback }): ts.EmitResult; - getLibrarySummaries(): LibrarySummary[]; } // Wrapper for createProgram. @@ -124,7 +119,7 @@ export function createProgram( {rootNames, options, host, oldProgram}: {rootNames: string[], options: CompilerOptions, host: CompilerHost, oldProgram?: Program}): Program { - return createProgramOrig({rootNames, options, host, oldProgram}); + return createProgramOrig({rootNames, options, host, oldProgram: oldProgram as ProgramOrig}); } // Wrapper for createCompilerHost. diff --git a/packages/compiler-cli/src/perform_compile.ts b/packages/compiler-cli/src/perform_compile.ts index 18fc69b9eb..0bfcf98fbb 100644 --- a/packages/compiler-cli/src/perform_compile.ts +++ b/packages/compiler-cli/src/perform_compile.ts @@ -13,11 +13,16 @@ import * as ts from 'typescript'; import * as api from './transformers/api'; import * as ng from './transformers/entry_points'; +import {createMessageDiagnostic} from './transformers/util'; const TS_EXT = /\.ts$/; export type Diagnostics = Array; +export function filterErrorsAndWarnings(diagnostics: Diagnostics): Diagnostics { + return diagnostics.filter(d => d.category !== ts.DiagnosticCategory.Message); +} + export function formatDiagnostics(options: api.CompilerOptions, diags: Diagnostics): string { if (diags && diags.length) { const tsFormatHost: ts.FormatDiagnosticsHost = { @@ -123,7 +128,7 @@ export interface PerformCompilationResult { } export function exitCodeFromResult(diags: Diagnostics | undefined): number { - if (!diags || diags.length === 0) { + if (!diags || filterErrorsAndWarnings(diags).length === 0) { // If we have a result and didn't get any errors, we succeeded. return 0; } @@ -154,7 +159,13 @@ export function performCompilation({rootNames, options, host, oldProgram, emitCa program = ng.createProgram({rootNames, host, options, oldProgram}); + const beforeDiags = Date.now(); allDiagnostics.push(...gatherDiagnostics(program !)); + if (options.diagnostics) { + const afterDiags = Date.now(); + allDiagnostics.push( + createMessageDiagnostic(`Time for diagnostics: ${afterDiags - beforeDiags}ms.`)); + } if (!hasErrors(allDiagnostics)) { emitResult = program !.emit({emitCallback, customTransformers, emitFlags}); diff --git a/packages/compiler-cli/src/perform_watch.ts b/packages/compiler-cli/src/perform_watch.ts index c83ae86b21..c5acc570f0 100644 --- a/packages/compiler-cli/src/perform_watch.ts +++ b/packages/compiler-cli/src/perform_watch.ts @@ -13,27 +13,7 @@ import * as ts from 'typescript'; import {Diagnostics, ParsedConfiguration, PerformCompilationResult, exitCodeFromResult, performCompilation, readConfiguration} from './perform_compile'; import * as api from './transformers/api'; import {createCompilerHost} from './transformers/entry_points'; - -const ChangeDiagnostics = { - Compilation_complete_Watching_for_file_changes: { - category: ts.DiagnosticCategory.Message, - messageText: 'Compilation complete. Watching for file changes.', - code: api.DEFAULT_ERROR_CODE, - source: api.SOURCE - }, - Compilation_failed_Watching_for_file_changes: { - category: ts.DiagnosticCategory.Message, - messageText: 'Compilation failed. Watching for file changes.', - code: api.DEFAULT_ERROR_CODE, - source: api.SOURCE - }, - File_change_detected_Starting_incremental_compilation: { - category: ts.DiagnosticCategory.Message, - messageText: 'File change detected. Starting incremental compilation.', - code: api.DEFAULT_ERROR_CODE, - source: api.SOURCE - }, -}; +import {createMessageDiagnostic} from './transformers/util'; function totalCompilationTimeDiagnostic(timeInMillis: number): api.Diagnostic { let duration: string; @@ -231,9 +211,11 @@ export function performWatchCompilation(host: PerformWatchHost): const exitCode = exitCodeFromResult(compileResult.diagnostics); if (exitCode == 0) { cachedProgram = compileResult.program; - host.reportDiagnostics([ChangeDiagnostics.Compilation_complete_Watching_for_file_changes]); + host.reportDiagnostics( + [createMessageDiagnostic('Compilation complete. Watching for file changes.')]); } else { - host.reportDiagnostics([ChangeDiagnostics.Compilation_failed_Watching_for_file_changes]); + host.reportDiagnostics( + [createMessageDiagnostic('Compilation failed. Watching for file changes.')]); } return compileResult.diagnostics; @@ -285,7 +267,7 @@ export function performWatchCompilation(host: PerformWatchHost): function recompile() { timerHandleForRecompilation = undefined; host.reportDiagnostics( - [ChangeDiagnostics.File_change_detected_Starting_incremental_compilation]); + [createMessageDiagnostic('File change detected. Starting incremental compilation.')]); doCompilation(); } } \ No newline at end of file diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 54bb786602..d5aff54dc9 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ParseSourceSpan} from '@angular/compiler'; +import {GeneratedFile, ParseSourceSpan} from '@angular/compiler'; import * as ts from 'typescript'; export const DEFAULT_ERROR_CODE = 100; @@ -220,6 +220,9 @@ export interface TsEmitArguments { export interface TsEmitCallback { (args: TsEmitArguments): ts.EmitResult; } +/** + * @internal + */ export interface LibrarySummary { fileName: string; text: string; @@ -306,6 +309,13 @@ export interface Program { * Returns the .d.ts / .ngsummary.json / .ngfactory.d.ts files of libraries that have been emitted * in this program or previous programs with paths that emulate the fact that these libraries * have been compiled before with no outDir. + * + * @internal */ - getLibrarySummaries(): LibrarySummary[]; + getLibrarySummaries(): Map; + + /** + * @internal + */ + getEmittedGeneratedFiles(): Map; } diff --git a/packages/compiler-cli/src/transformers/compiler_host.ts b/packages/compiler-cli/src/transformers/compiler_host.ts index 85e7702f4c..acdf9f5c0d 100644 --- a/packages/compiler-cli/src/transformers/compiler_host.ts +++ b/packages/compiler-cli/src/transformers/compiler_host.ts @@ -58,7 +58,6 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends private generatedSourceFiles = new Map(); private generatedCodeFor = new Map(); private emitter = new TypeScriptEmitter(); - private librarySummaries = new Map(); getCancellationToken: () => ts.CancellationToken; getDefaultLibLocation: () => string; trace: (s: string) => void; @@ -68,9 +67,8 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends constructor( private rootFiles: string[], options: CompilerOptions, context: CompilerHost, private metadataProvider: MetadataProvider, private codeGenerator: CodeGenerator, - librarySummaries: LibrarySummary[]) { + private librarySummaries = new Map()) { super(options, context); - librarySummaries.forEach(summary => this.librarySummaries.set(summary.fileName, summary)); this.moduleResolutionCache = ts.createModuleResolutionCache( this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context)); const basePath = this.options.basePath !; diff --git a/packages/compiler-cli/src/transformers/node_emitter_transform.ts b/packages/compiler-cli/src/transformers/node_emitter_transform.ts index 47b9354ade..0153e90848 100644 --- a/packages/compiler-cli/src/transformers/node_emitter_transform.ts +++ b/packages/compiler-cli/src/transformers/node_emitter_transform.ts @@ -10,6 +10,7 @@ import {GeneratedFile} from '@angular/compiler'; import * as ts from 'typescript'; import {TypeScriptNodeEmitter} from './node_emitter'; +import {GENERATED_FILES} from './util'; const PREAMBLE = `/** * @fileoverview This file is generated by the Angular template compiler. @@ -18,17 +19,17 @@ const PREAMBLE = `/** * tslint:disable */`; -export function getAngularEmitterTransformFactory(generatedFiles: GeneratedFile[]): () => +export function getAngularEmitterTransformFactory(generatedFiles: Map): () => (sourceFile: ts.SourceFile) => ts.SourceFile { return function() { - const map = new Map(generatedFiles.filter(g => g.stmts && g.stmts.length) - .map<[string, GeneratedFile]>(g => [g.genFileUrl, g])); const emitter = new TypeScriptNodeEmitter(); return function(sourceFile: ts.SourceFile): ts.SourceFile { - const g = map.get(sourceFile.fileName); + const g = generatedFiles.get(sourceFile.fileName); if (g && g.stmts) { const [newSourceFile] = emitter.updateSourceFile(sourceFile, g.stmts, PREAMBLE); return newSourceFile; + } else if (GENERATED_FILES.test(sourceFile.fileName)) { + return ts.updateSourceFileNode(sourceFile, []); } return sourceFile; }; diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 54761e238a..205a4f40f7 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -18,7 +18,13 @@ import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, D import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host'; import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions'; import {getAngularEmitterTransformFactory} from './node_emitter_transform'; -import {GENERATED_FILES, StructureIsReused, tsStructureIsReused} from './util'; +import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, tsStructureIsReused} from './util'; + +/** + * Maximum number of files that are emitable via calling ts.Program.emit + * passing individual targetSourceFiles. + */ +const MAX_FILE_COUNT_FOR_SINGLE_FILE_EMIT = 20; const emptyModules: NgAnalyzedModules = { ngModules: [], @@ -32,18 +38,20 @@ const defaultEmitCallback: TsEmitCallback = program.emit( targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); - class AngularCompilerProgram implements Program { private metadataCache: LowerMetadataCache; - private oldProgramLibrarySummaries: LibrarySummary[] = []; + private oldProgramLibrarySummaries: Map|undefined; + private oldProgramEmittedGeneratedFiles: Map|undefined; // Note: This will be cleared out as soon as we create the _tsProgram private oldTsProgram: ts.Program|undefined; private emittedLibrarySummaries: LibrarySummary[]|undefined; + private emittedGeneratedFiles: GeneratedFile[]|undefined; // Lazily initialized fields private _typeCheckHost: TypeCheckHost; private _compiler: AotCompiler; private _tsProgram: ts.Program; + private _changedNonGenFileNames: string[]|undefined; private _analyzedModules: NgAnalyzedModules|undefined; private _structuralDiagnostics: Diagnostic[]|undefined; private _programWithStubs: ts.Program|undefined; @@ -60,6 +68,7 @@ class AngularCompilerProgram implements Program { this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined; if (oldProgram) { this.oldProgramLibrarySummaries = oldProgram.getLibrarySummaries(); + this.oldProgramEmittedGeneratedFiles = oldProgram.getEmittedGeneratedFiles(); } if (options.flatModuleOutFile) { @@ -81,10 +90,26 @@ class AngularCompilerProgram implements Program { this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit); } - getLibrarySummaries(): LibrarySummary[] { - const result = [...this.oldProgramLibrarySummaries]; + getLibrarySummaries(): Map { + const result = new Map(); + if (this.oldProgramLibrarySummaries) { + this.oldProgramLibrarySummaries.forEach((summary, fileName) => result.set(fileName, summary)); + } if (this.emittedLibrarySummaries) { - result.push(...this.emittedLibrarySummaries); + this.emittedLibrarySummaries.forEach( + (summary, fileName) => result.set(summary.fileName, summary)); + } + return result; + } + + getEmittedGeneratedFiles(): Map { + const result = new Map(); + if (this.oldProgramEmittedGeneratedFiles) { + this.oldProgramEmittedGeneratedFiles.forEach( + (genFile, fileName) => result.set(fileName, genFile)); + } + if (this.emittedGeneratedFiles) { + this.emittedGeneratedFiles.forEach((genFile) => result.set(genFile.genFileUrl, genFile)); } return result; } @@ -142,6 +167,7 @@ class AngularCompilerProgram implements Program { customTransformers?: CustomTransformers, emitCallback?: TsEmitCallback } = {}): ts.EmitResult { + const emitStart = Date.now(); if (emitFlags & EmitFlags.I18nBundle) { const locale = this.options.i18nOutLocale || null; const file = this.options.i18nOutFile || null; @@ -153,7 +179,7 @@ class AngularCompilerProgram implements Program { 0) { return {emitSkipped: true, diagnostics: [], emittedFiles: []}; } - const {genFiles, genDiags} = this.generateFilesForEmit(emitFlags); + let {genFiles, genDiags} = this.generateFilesForEmit(emitFlags); if (genDiags.length) { return { diagnostics: genDiags, @@ -161,11 +187,11 @@ class AngularCompilerProgram implements Program { emittedFiles: [], }; } - const emittedLibrarySummaries = this.emittedLibrarySummaries = []; - + this.emittedGeneratedFiles = genFiles; const outSrcMapping: Array<{sourceFile: ts.SourceFile, outFileName: string}> = []; const genFileByFileName = new Map(); genFiles.forEach(genFile => genFileByFileName.set(genFile.genFileUrl, genFile)); + this.emittedLibrarySummaries = []; const writeTsFile: ts.WriteFileCallback = (outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => { const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null; @@ -176,7 +202,8 @@ class AngularCompilerProgram implements Program { } this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles); }; - + const tsCustomTansformers = this.calculateTransforms(genFileByFileName, customTransformers); + const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS; // Restore the original references before we emit so TypeScript doesn't emit // a reference to the .d.ts file. const augmentedReferences = new Map(); @@ -187,16 +214,44 @@ class AngularCompilerProgram implements Program { sourceFile.referencedFiles = originalReferences; } } + const genTsFiles: GeneratedFile[] = []; + const genJsonFiles: GeneratedFile[] = []; + genFiles.forEach(gf => { + if (gf.stmts) { + genTsFiles.push(gf); + } + if (gf.source) { + genJsonFiles.push(gf); + } + }); let emitResult: ts.EmitResult; + let emittedUserTsCount: number; try { - emitResult = emitCallback({ - program: this.tsProgram, - host: this.host, - options: this.options, - writeFile: writeTsFile, - emitOnlyDtsFiles: (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS, - customTransformers: this.calculateTransforms(genFiles, customTransformers) - }); + const emitChangedFilesOnly = this._changedNonGenFileNames && + this._changedNonGenFileNames.length < MAX_FILE_COUNT_FOR_SINGLE_FILE_EMIT; + if (emitChangedFilesOnly) { + const fileNamesToEmit = + [...this._changedNonGenFileNames !, ...genTsFiles.map(gf => gf.genFileUrl)]; + emitResult = mergeEmitResults( + fileNamesToEmit.map((fileName) => emitResult = emitCallback({ + program: this.tsProgram, + host: this.host, + options: this.options, + writeFile: writeTsFile, emitOnlyDtsFiles, + customTransformers: tsCustomTansformers, + targetSourceFile: this.tsProgram.getSourceFile(fileName), + }))); + emittedUserTsCount = this._changedNonGenFileNames !.length; + } else { + emitResult = emitCallback({ + program: this.tsProgram, + host: this.host, + options: this.options, + writeFile: writeTsFile, emitOnlyDtsFiles, + customTransformers: tsCustomTansformers + }); + emittedUserTsCount = this.tsProgram.getSourceFiles().length - genTsFiles.length; + } } finally { // Restore the references back to the augmented value to ensure that the // checks that TypeScript makes for project structure reuse will succeed. @@ -207,10 +262,10 @@ class AngularCompilerProgram implements Program { if (!outSrcMapping.length) { // if no files were emitted by TypeScript, also don't emit .json files + emitResult.diagnostics.push(createMessageDiagnostic(`Emitted no files.`)); return emitResult; } - let sampleSrcFileName: string|undefined; let sampleOutFileName: string|undefined; if (outSrcMapping.length) { @@ -220,16 +275,16 @@ class AngularCompilerProgram implements Program { const srcToOutPath = createSrcToOutPathMapper(this.options.outDir, sampleSrcFileName, sampleOutFileName); if (emitFlags & EmitFlags.Codegen) { - genFiles.forEach(gf => { - if (gf.source) { - const outFileName = srcToOutPath(gf.genFileUrl); - this.writeFile(outFileName, gf.source, false, undefined, gf); - } + genJsonFiles.forEach(gf => { + const outFileName = srcToOutPath(gf.genFileUrl); + this.writeFile(outFileName, gf.source !, false, undefined, gf); }); } + let metadataJsonCount = 0; if (emitFlags & EmitFlags.Metadata) { this.tsProgram.getSourceFiles().forEach(sf => { if (!sf.isDeclarationFile && !GENERATED_FILES.test(sf.fileName)) { + metadataJsonCount++; const metadata = this.metadataCache.getMetadata(sf); const metadataText = JSON.stringify([metadata]); const outFileName = srcToOutPath(sf.fileName.replace(/\.ts$/, '.metadata.json')); @@ -237,6 +292,15 @@ class AngularCompilerProgram implements Program { } }); } + const emitEnd = Date.now(); + if (this.options.diagnostics) { + emitResult.diagnostics.push(createMessageDiagnostic([ + `Emitted in ${emitEnd - emitStart}ms`, + `- ${emittedUserTsCount} user ts files`, + `- ${genTsFiles.length} generated ts files`, + `- ${genJsonFiles.length + metadataJsonCount} generated json files`, + ].join('\n'))); + } return emitResult; } @@ -281,8 +345,9 @@ class AngularCompilerProgram implements Program { (this._semanticDiagnostics = this.generateSemanticDiagnostics()); } - private calculateTransforms(genFiles: GeneratedFile[], customTransformers?: CustomTransformers): - ts.CustomTransformers { + private calculateTransforms( + genFiles: Map, + customTransformers?: CustomTransformers): ts.CustomTransformers { const beforeTs: ts.TransformerFactory[] = []; if (!this.options.disableExpressionLowering) { beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache)); @@ -353,6 +418,16 @@ class AngularCompilerProgram implements Program { sourceFiles.push(sf.fileName); } }); + if (oldTsProgram) { + // TODO(tbosch): if one of the files contains a `const enum` + // always emit all files! + const changedNonGenFileNames = this._changedNonGenFileNames = [] as string[]; + tmpProgram.getSourceFiles().forEach(sf => { + if (!GENERATED_FILES.test(sf.fileName) && oldTsProgram.getSourceFile(sf.fileName) !== sf) { + changedNonGenFileNames.push(sf.fileName); + } + }); + } return {tmpProgram, sourceFiles, hostAdapter, rootNames}; } @@ -418,7 +493,14 @@ class AngularCompilerProgram implements Program { if (!(emitFlags & EmitFlags.Codegen)) { return {genFiles: [], genDiags: []}; } - const genFiles = this.compiler.emitAllImpls(this.analyzedModules); + let genFiles = this.compiler.emitAllImpls(this.analyzedModules); + if (this.oldProgramEmittedGeneratedFiles) { + const oldProgramEmittedGeneratedFiles = this.oldProgramEmittedGeneratedFiles; + genFiles = genFiles.filter(genFile => { + const oldGenFile = oldProgramEmittedGeneratedFiles.get(genFile.genFileUrl); + return !oldGenFile || !genFile.isEquivalent(oldGenFile); + }); + } return {genFiles, genDiags: []}; } catch (e) { // TODO(tbosch): check whether we can actually have syntax errors here, @@ -649,3 +731,15 @@ export function i18nGetExtension(formatName: string): string { throw new Error(`Unsupported format "${formatName}"`); } + +function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult { + const diagnostics: ts.Diagnostic[] = []; + let emitSkipped = true; + const emittedFiles: string[] = []; + for (const er of emitResults) { + diagnostics.push(...er.diagnostics); + emitSkipped = emitSkipped || er.emitSkipped; + emittedFiles.push(...er.emittedFiles); + } + return {diagnostics, emitSkipped, emittedFiles}; +} diff --git a/packages/compiler-cli/src/transformers/util.ts b/packages/compiler-cli/src/transformers/util.ts index bd2b78b71c..ec774fcde3 100644 --- a/packages/compiler-cli/src/transformers/util.ts +++ b/packages/compiler-cli/src/transformers/util.ts @@ -8,6 +8,8 @@ import * as ts from 'typescript'; +import {DEFAULT_ERROR_CODE, Diagnostic, SOURCE} from './api'; + export const GENERATED_FILES = /(.*?)\.(ngfactory|shim\.ngstyle|ngstyle|ngsummary)\.(js|d\.ts|ts)$/; export const enum StructureIsReused {Not = 0, SafeModules = 1, Completely = 2} @@ -15,4 +17,15 @@ export const enum StructureIsReused {Not = 0, SafeModules = 1, Completely = 2} // Note: This is an internal property in TypeScript. Use it only for assertions and tests. export function tsStructureIsReused(program: ts.Program): StructureIsReused { return (program as any).structureIsReused; -} \ No newline at end of file +} + +export function createMessageDiagnostic(messageText: string): ts.Diagnostic&Diagnostic { + return { + file: undefined, + start: undefined, + length: undefined, + category: ts.DiagnosticCategory.Message, messageText, + code: DEFAULT_ERROR_CODE, + source: SOURCE, + }; +} diff --git a/packages/compiler-cli/test/perform_watch_spec.ts b/packages/compiler-cli/test/perform_watch_spec.ts index 6bac15f01c..ae0184e2c2 100644 --- a/packages/compiler-cli/test/perform_watch_spec.ts +++ b/packages/compiler-cli/test/perform_watch_spec.ts @@ -86,10 +86,9 @@ describe('perform watch', () => { // trigger a single file change // -> all other files should be cached - fs.unlinkSync(mainNgFactory); host.triggerFileChange(FileChangeEvent.Change, utilTsPath); + expectNoDiagnostics(config.options, host.diagnostics); - expect(fs.existsSync(mainNgFactory)).toBe(true); expect(fileExistsSpy !).not.toHaveBeenCalledWith(mainTsPath); expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath); expect(getSourceFileSpy !).not.toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5); @@ -97,11 +96,10 @@ describe('perform watch', () => { // trigger a folder change // -> nothing should be cached - fs.unlinkSync(mainNgFactory); host.triggerFileChange( FileChangeEvent.CreateDeleteDir, path.resolve(testSupport.basePath, 'src')); + expectNoDiagnostics(config.options, host.diagnostics); - expect(fs.existsSync(mainNgFactory)).toBe(true); expect(fileExistsSpy !).toHaveBeenCalledWith(mainTsPath); expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath); expect(getSourceFileSpy !).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5); diff --git a/packages/compiler-cli/test/test_support.ts b/packages/compiler-cli/test/test_support.ts index 8845e2275d..38dd9e9678 100644 --- a/packages/compiler-cli/test/test_support.ts +++ b/packages/compiler-cli/test/test_support.ts @@ -110,8 +110,9 @@ export function setup(): TestSupport { } export function expectNoDiagnostics(options: ng.CompilerOptions, diags: ng.Diagnostics) { - if (diags.length) { - throw new Error(`Expected no diagnostics: ${ng.formatDiagnostics(options, diags)}`); + const errorDiags = diags.filter(d => d.category !== ts.DiagnosticCategory.Message); + if (errorDiags.length) { + throw new Error(`Expected no diagnostics: ${ng.formatDiagnostics(options, errorDiags)}`); } } diff --git a/packages/compiler-cli/test/transformers/compiler_host_spec.ts b/packages/compiler-cli/test/transformers/compiler_host_spec.ts index e2579a7e53..d597fb8ce2 100644 --- a/packages/compiler-cli/test/transformers/compiler_host_spec.ts +++ b/packages/compiler-cli/test/transformers/compiler_host_spec.ts @@ -51,7 +51,7 @@ describe('NgCompilerHost', () => { } = {}) { return new TsCompilerAotCompilerTypeCheckHostAdapter( ['/tmp/index.ts'], options, ngHost, new MetadataCollector(), codeGenerator, - librarySummaries); + new Map(librarySummaries.map(entry => [entry.fileName, entry] as[string, LibrarySummary]))); } describe('fileNameToModuleName', () => { diff --git a/packages/compiler-cli/test/transformers/program_spec.ts b/packages/compiler-cli/test/transformers/program_spec.ts index eca0ae9207..def4fd1db4 100644 --- a/packages/compiler-cli/test/transformers/program_spec.ts +++ b/packages/compiler-cli/test/transformers/program_spec.ts @@ -57,17 +57,20 @@ describe('ng program', () => { } function compile( - oldProgram?: ng.Program, overrideOptions?: ng.CompilerOptions, - rootNames?: string[]): ng.Program { + oldProgram?: ng.Program, overrideOptions?: ng.CompilerOptions, rootNames?: string[], + host?: CompilerHost): ng.Program { const options = testSupport.createCompilerOptions(overrideOptions); if (!rootNames) { rootNames = [path.resolve(testSupport.basePath, 'src/index.ts')]; } - + if (!host) { + host = ng.createCompilerHost({options}); + } const program = ng.createProgram({ rootNames: rootNames, options, - host: ng.createCompilerHost({options}), oldProgram, + host, + oldProgram, }); expectNoDiagnosticsInProgram(options, program); program.emit(); @@ -153,6 +156,59 @@ describe('ng program', () => { .toBe(false); }); + it('should only emit changed files', () => { + testSupport.writeFiles({ + 'src/index.ts': createModuleAndCompSource('comp', 'index.html'), + 'src/index.html': `Start` + }); + const options: ng.CompilerOptions = {declaration: false}; + const host = ng.createCompilerHost({options}); + const originalGetSourceFile = host.getSourceFile; + const fileCache = new Map(); + host.getSourceFile = (fileName: string) => { + if (fileCache.has(fileName)) { + return fileCache.get(fileName); + } + const sf = originalGetSourceFile.call(host, fileName); + fileCache.set(fileName, sf); + return sf; + }; + + const written = new Map(); + host.writeFile = (fileName: string, data: string) => written.set(fileName, data); + + // compile libraries + const p1 = compile(undefined, options, undefined, host); + + // first compile without libraries + const p2 = compile(p1, options, undefined, host); + expect(written.has(path.resolve(testSupport.basePath, 'built/src/index.js'))).toBe(true); + let ngFactoryContent = + written.get(path.resolve(testSupport.basePath, 'built/src/index.ngfactory.js')); + expect(ngFactoryContent).toMatch(/Start/); + + // no change -> no emit + written.clear(); + const p3 = compile(p2, options, undefined, host); + expect(written.size).toBe(0); + + // change a user file + written.clear(); + fileCache.delete(path.resolve(testSupport.basePath, 'src/index.ts')); + const p4 = compile(p3, options, undefined, host); + expect(written.size).toBe(1); + expect(written.has(path.resolve(testSupport.basePath, 'built/src/index.js'))).toBe(true); + + // change a file that is input to generated files + written.clear(); + testSupport.writeFiles({'src/index.html': 'Hello'}); + const p5 = compile(p4, options, undefined, host); + expect(written.size).toBe(1); + ngFactoryContent = + written.get(path.resolve(testSupport.basePath, 'built/src/index.ngfactory.js')); + expect(ngFactoryContent).toMatch(/Hello/); + }); + it('should store library summaries on emit', () => { compileLib('lib'); testSupport.writeFiles({ @@ -163,17 +219,19 @@ describe('ng program', () => { ` }); const p1 = compile(); - expect(p1.getLibrarySummaries().some( - sf => /node_modules\/lib\/index\.ngfactory\.d\.ts$/.test(sf.fileName))) + expect(Array.from(p1.getLibrarySummaries().values()) + .some(sf => /node_modules\/lib\/index\.ngfactory\.d\.ts$/.test(sf.fileName))) .toBe(true); - expect(p1.getLibrarySummaries().some( - sf => /node_modules\/lib\/index\.ngsummary\.json$/.test(sf.fileName))) + expect(Array.from(p1.getLibrarySummaries().values()) + .some(sf => /node_modules\/lib\/index\.ngsummary\.json$/.test(sf.fileName))) .toBe(true); - expect( - p1.getLibrarySummaries().some(sf => /node_modules\/lib\/index\.d\.ts$/.test(sf.fileName))) + expect(Array.from(p1.getLibrarySummaries().values()) + .some(sf => /node_modules\/lib\/index\.d\.ts$/.test(sf.fileName))) .toBe(true); - expect(p1.getLibrarySummaries().some(sf => /src\/main.*$/.test(sf.fileName))).toBe(false); + expect(Array.from(p1.getLibrarySummaries().values()) + .some(sf => /src\/main.*$/.test(sf.fileName))) + .toBe(false); }); it('should reuse the old ts program completely if nothing changed', () => { diff --git a/packages/compiler/src/aot/generated_file.ts b/packages/compiler/src/aot/generated_file.ts index 226126f4f4..c072e57a90 100644 --- a/packages/compiler/src/aot/generated_file.ts +++ b/packages/compiler/src/aot/generated_file.ts @@ -7,7 +7,7 @@ */ import {sourceUrl} from '../compile_metadata'; -import {Statement} from '../output/output_ast'; +import {Statement, areAllEquivalent} from '../output/output_ast'; import {TypeScriptEmitter} from '../output/ts_emitter'; export class GeneratedFile { @@ -24,6 +24,21 @@ export class GeneratedFile { this.stmts = sourceOrStmts; } } + + isEquivalent(other: GeneratedFile): boolean { + if (this.genFileUrl !== other.genFileUrl) { + return false; + } + if (this.source) { + return this.source === other.source; + } + if (other.stmts == null) { + return false; + } + // Note: the constructor guarantees that if this.source is not filled, + // then this.stmts is. + return areAllEquivalent(this.stmts !, other.stmts !); + } } export function toTypeScript(file: GeneratedFile, preamble: string = ''): string { diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index afa3567f19..ab0e561d56 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -104,6 +104,27 @@ export enum BinaryOperator { BiggerEquals } +export function nullSafeIsEquivalent( + base: T | null, other: T | null) { + if (base == null || other == null) { + return base == other; + } + return base.isEquivalent(other); +} + +export function areAllEquivalent( + base: T[], other: T[]) { + const len = base.length; + if (len !== other.length) { + return false; + } + for (let i = 0; i < len; i++) { + if (!base[i].isEquivalent(other[i])) { + return false; + } + } + return true; +} export abstract class Expression { public type: Type|null; @@ -116,6 +137,12 @@ export abstract class Expression { abstract visitExpression(visitor: ExpressionVisitor, context: any): any; + /** + * Calculates whether this expression produces the same value as the given expression. + * Note: We don't check Types nor ParseSourceSpans nor function arguments. + */ + abstract isEquivalent(e: Expression): boolean; + prop(name: string, sourceSpan?: ParseSourceSpan|null): ReadPropExpr { return new ReadPropExpr(this, name, null, sourceSpan); } @@ -222,6 +249,10 @@ export class ReadVarExpr extends Expression { this.builtin = name; } } + isEquivalent(e: Expression): boolean { + return e instanceof ReadVarExpr && this.name === e.name && this.builtin === e.builtin; + } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitReadVarExpr(this, context); } @@ -242,6 +273,9 @@ export class WriteVarExpr extends Expression { super(type || value.type, sourceSpan); this.value = value; } + isEquivalent(e: Expression): boolean { + return e instanceof WriteVarExpr && this.name === e.name && this.value.isEquivalent(e.value); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitWriteVarExpr(this, context); @@ -261,6 +295,10 @@ export class WriteKeyExpr extends Expression { super(type || value.type, sourceSpan); this.value = value; } + isEquivalent(e: Expression): boolean { + return e instanceof WriteKeyExpr && this.receiver.isEquivalent(e.receiver) && + this.index.isEquivalent(e.index) && this.value.isEquivalent(e.value); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitWriteKeyExpr(this, context); } @@ -275,6 +313,10 @@ export class WritePropExpr extends Expression { super(type || value.type, sourceSpan); this.value = value; } + isEquivalent(e: Expression): boolean { + return e instanceof WritePropExpr && this.receiver.isEquivalent(e.receiver) && + this.name === e.name && this.value.isEquivalent(e.value); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitWritePropExpr(this, context); } @@ -301,6 +343,10 @@ export class InvokeMethodExpr extends Expression { this.builtin = method; } } + isEquivalent(e: Expression): boolean { + return e instanceof InvokeMethodExpr && this.receiver.isEquivalent(e.receiver) && + this.name === e.name && this.builtin === e.builtin && areAllEquivalent(this.args, e.args); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitInvokeMethodExpr(this, context); } @@ -313,6 +359,10 @@ export class InvokeFunctionExpr extends Expression { sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { + return e instanceof InvokeFunctionExpr && this.fn.isEquivalent(e.fn) && + areAllEquivalent(this.args, e.args); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitInvokeFunctionExpr(this, context); } @@ -325,6 +375,10 @@ export class InstantiateExpr extends Expression { sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { + return e instanceof InstantiateExpr && this.classExpr.isEquivalent(e.classExpr) && + areAllEquivalent(this.args, e.args); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitInstantiateExpr(this, context); } @@ -332,9 +386,14 @@ export class InstantiateExpr extends Expression { export class LiteralExpr extends Expression { - constructor(public value: any, type?: Type|null, sourceSpan?: ParseSourceSpan|null) { + constructor( + public value: number|string|boolean|null|undefined, type?: Type|null, + sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { + return e instanceof LiteralExpr && this.value === e.value; + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitLiteralExpr(this, context); } @@ -347,6 +406,10 @@ export class ExternalExpr extends Expression { sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { + return e instanceof ExternalExpr && this.value.name === e.value.name && + this.value.moduleName === e.value.moduleName && this.value.runtime === e.value.runtime; + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitExternalExpr(this, context); } @@ -355,6 +418,7 @@ export class ExternalExpr extends Expression { export class ExternalReference { constructor(public moduleName: string|null, public name: string|null, public runtime?: any|null) { } + // Note: no isEquivalent method here as we use this as an interface too. } export class ConditionalExpr extends Expression { @@ -365,6 +429,10 @@ export class ConditionalExpr extends Expression { super(type || trueCase.type, sourceSpan); this.trueCase = trueCase; } + isEquivalent(e: Expression): boolean { + return e instanceof ConditionalExpr && this.condition.isEquivalent(e.condition) && + this.trueCase.isEquivalent(e.trueCase) && nullSafeIsEquivalent(this.falseCase, e.falseCase); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitConditionalExpr(this, context); } @@ -375,6 +443,9 @@ export class NotExpr extends Expression { constructor(public condition: Expression, sourceSpan?: ParseSourceSpan|null) { super(BOOL_TYPE, sourceSpan); } + isEquivalent(e: Expression): boolean { + return e instanceof NotExpr && this.condition.isEquivalent(e.condition); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitNotExpr(this, context); } @@ -384,6 +455,9 @@ export class AssertNotNull extends Expression { constructor(public condition: Expression, sourceSpan?: ParseSourceSpan|null) { super(condition.type, sourceSpan); } + isEquivalent(e: Expression): boolean { + return e instanceof AssertNotNull && this.condition.isEquivalent(e.condition); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitAssertNotNullExpr(this, context); } @@ -393,6 +467,9 @@ export class CastExpr extends Expression { constructor(public value: Expression, type?: Type|null, sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { + return e instanceof CastExpr && this.value.isEquivalent(e.value); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitCastExpr(this, context); } @@ -401,6 +478,8 @@ export class CastExpr extends Expression { export class FnParam { constructor(public name: string, public type: Type|null = null) {} + + isEquivalent(param: FnParam): boolean { return this.name === param.name; } } @@ -410,6 +489,10 @@ export class FunctionExpr extends Expression { sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { + return e instanceof FunctionExpr && areAllEquivalent(this.params, e.params) && + areAllEquivalent(this.statements, e.statements); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitFunctionExpr(this, context); } @@ -429,6 +512,10 @@ export class BinaryOperatorExpr extends Expression { super(type || lhs.type, sourceSpan); this.lhs = lhs; } + isEquivalent(e: Expression): boolean { + return e instanceof BinaryOperatorExpr && this.operator === e.operator && + this.lhs.isEquivalent(e.lhs) && this.rhs.isEquivalent(e.rhs); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitBinaryOperatorExpr(this, context); } @@ -441,6 +528,10 @@ export class ReadPropExpr extends Expression { sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { + return e instanceof ReadPropExpr && this.receiver.isEquivalent(e.receiver) && + this.name === e.name; + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitReadPropExpr(this, context); } @@ -456,6 +547,10 @@ export class ReadKeyExpr extends Expression { sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { + return e instanceof ReadKeyExpr && this.receiver.isEquivalent(e.receiver) && + this.index.isEquivalent(e.index); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitReadKeyExpr(this, context); } @@ -471,6 +566,9 @@ export class LiteralArrayExpr extends Expression { super(type, sourceSpan); this.entries = entries; } + isEquivalent(e: Expression): boolean { + return e instanceof LiteralArrayExpr && areAllEquivalent(this.entries, e.entries); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitLiteralArrayExpr(this, context); } @@ -478,6 +576,9 @@ export class LiteralArrayExpr extends Expression { export class LiteralMapEntry { constructor(public key: string, public value: Expression, public quoted: boolean) {} + isEquivalent(e: LiteralMapEntry): boolean { + return this.key === e.key && this.value.isEquivalent(e.value); + } } export class LiteralMapExpr extends Expression { @@ -489,6 +590,9 @@ export class LiteralMapExpr extends Expression { this.valueType = type.valueType; } } + isEquivalent(e: Expression): boolean { + return e instanceof LiteralMapExpr && areAllEquivalent(this.entries, e.entries); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitLiteralMapExpr(this, context); } @@ -498,6 +602,9 @@ export class CommaExpr extends Expression { constructor(public parts: Expression[], sourceSpan?: ParseSourceSpan|null) { super(parts[parts.length - 1].type, sourceSpan); } + isEquivalent(e: Expression): boolean { + return e instanceof CommaExpr && areAllEquivalent(this.parts, e.parts); + } visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitCommaExpr(this, context); } @@ -547,6 +654,11 @@ export abstract class Statement { this.modifiers = modifiers || []; this.sourceSpan = sourceSpan || null; } + /** + * Calculates whether this statement produces the same value as the given statement. + * Note: We don't check Types nor ParseSourceSpans nor function arguments. + */ + abstract isEquivalent(stmt: Statement): boolean; abstract visitStatement(visitor: StatementVisitor, context: any): any; @@ -562,7 +674,10 @@ export class DeclareVarStmt extends Statement { super(modifiers, sourceSpan); this.type = type || value.type; } - + isEquivalent(stmt: Statement): boolean { + return stmt instanceof DeclareVarStmt && this.name === stmt.name && + this.value.isEquivalent(stmt.value); + } visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitDeclareVarStmt(this, context); } @@ -576,6 +691,10 @@ export class DeclareFunctionStmt extends Statement { super(modifiers, sourceSpan); this.type = type || null; } + isEquivalent(stmt: Statement): boolean { + return stmt instanceof DeclareFunctionStmt && areAllEquivalent(this.params, stmt.params) && + areAllEquivalent(this.statements, stmt.statements); + } visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitDeclareFunctionStmt(this, context); @@ -586,6 +705,9 @@ export class ExpressionStatement extends Statement { constructor(public expr: Expression, sourceSpan?: ParseSourceSpan|null) { super(null, sourceSpan); } + isEquivalent(stmt: Statement): boolean { + return stmt instanceof ExpressionStatement && this.expr.isEquivalent(stmt.expr); + } visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitExpressionStmt(this, context); @@ -597,6 +719,9 @@ export class ReturnStatement extends Statement { constructor(public value: Expression, sourceSpan?: ParseSourceSpan|null) { super(null, sourceSpan); } + isEquivalent(stmt: Statement): boolean { + return stmt instanceof ReturnStatement && this.value.isEquivalent(stmt.value); + } visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitReturnStmt(this, context); } @@ -617,6 +742,7 @@ export class ClassField extends AbstractClassPart { constructor(public name: string, type?: Type|null, modifiers: StmtModifier[]|null = null) { super(type, modifiers); } + isEquivalent(f: ClassField) { return this.name === f.name; } } @@ -626,6 +752,9 @@ export class ClassMethod extends AbstractClassPart { type?: Type|null, modifiers: StmtModifier[]|null = null) { super(type, modifiers); } + isEquivalent(m: ClassMethod) { + return this.name === m.name && areAllEquivalent(this.body, m.body); + } } @@ -635,6 +764,9 @@ export class ClassGetter extends AbstractClassPart { modifiers: StmtModifier[]|null = null) { super(type, modifiers); } + isEquivalent(m: ClassGetter) { + return this.name === m.name && areAllEquivalent(this.body, m.body); + } } @@ -646,6 +778,14 @@ export class ClassStmt extends Statement { sourceSpan?: ParseSourceSpan|null) { super(modifiers, sourceSpan); } + isEquivalent(stmt: Statement): boolean { + return stmt instanceof ClassStmt && this.name === stmt.name && + nullSafeIsEquivalent(this.parent, stmt.parent) && + areAllEquivalent(this.fields, stmt.fields) && + areAllEquivalent(this.getters, stmt.getters) && + this.constructorMethod.isEquivalent(stmt.constructorMethod) && + areAllEquivalent(this.methods, stmt.methods); + } visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitDeclareClassStmt(this, context); } @@ -658,6 +798,11 @@ export class IfStmt extends Statement { public falseCase: Statement[] = [], sourceSpan?: ParseSourceSpan|null) { super(null, sourceSpan); } + isEquivalent(stmt: Statement): boolean { + return stmt instanceof IfStmt && this.condition.isEquivalent(stmt.condition) && + areAllEquivalent(this.trueCase, stmt.trueCase) && + areAllEquivalent(this.falseCase, stmt.falseCase); + } visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitIfStmt(this, context); } @@ -668,6 +813,7 @@ export class CommentStmt extends Statement { constructor(public comment: string, sourceSpan?: ParseSourceSpan|null) { super(null, sourceSpan); } + isEquivalent(stmt: Statement): boolean { return stmt instanceof CommentStmt; } visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitCommentStmt(this, context); } @@ -680,6 +826,10 @@ export class TryCatchStmt extends Statement { sourceSpan?: ParseSourceSpan|null) { super(null, sourceSpan); } + isEquivalent(stmt: Statement): boolean { + return stmt instanceof TryCatchStmt && areAllEquivalent(this.bodyStmts, stmt.bodyStmts) && + areAllEquivalent(this.catchStmts, stmt.catchStmts); + } visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitTryCatchStmt(this, context); } @@ -690,6 +840,9 @@ export class ThrowStmt extends Statement { constructor(public error: Expression, sourceSpan?: ParseSourceSpan|null) { super(null, sourceSpan); } + isEquivalent(stmt: ThrowStmt): boolean { + return stmt instanceof TryCatchStmt && this.error.isEquivalent(stmt.error); + } visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitThrowStmt(this, context); }