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); }