perf(compiler): only emit changed files for incremental compilation
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.
This commit is contained in:
parent
b0868915ae
commit
745b59f49c
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<ts.Diagnostic|api.Diagnostic>;
|
||||
|
||||
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});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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<string, LibrarySummary>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getEmittedGeneratedFiles(): Map<string, GeneratedFile>;
|
||||
}
|
||||
|
|
|
@ -58,7 +58,6 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
|
|||
private generatedSourceFiles = new Map<string, GenSourceFile>();
|
||||
private generatedCodeFor = new Map<string, string[]>();
|
||||
private emitter = new TypeScriptEmitter();
|
||||
private librarySummaries = new Map<string, LibrarySummary>();
|
||||
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<string, LibrarySummary>()) {
|
||||
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 !;
|
||||
|
|
|
@ -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<string, GeneratedFile>): () =>
|
||||
(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;
|
||||
};
|
||||
|
|
|
@ -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<string, LibrarySummary>|undefined;
|
||||
private oldProgramEmittedGeneratedFiles: Map<string, GeneratedFile>|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<string, LibrarySummary> {
|
||||
const result = new Map<string, LibrarySummary>();
|
||||
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<string, GeneratedFile> {
|
||||
const result = new Map<string, GeneratedFile>();
|
||||
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<string, GeneratedFile>();
|
||||
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<ts.SourceFile, ts.FileReference[]>();
|
||||
|
@ -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<string, GeneratedFile>,
|
||||
customTransformers?: CustomTransformers): ts.CustomTransformers {
|
||||
const beforeTs: ts.TransformerFactory<ts.SourceFile>[] = [];
|
||||
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};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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<string, ts.SourceFile>();
|
||||
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<string, string>();
|
||||
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', () => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -104,6 +104,27 @@ export enum BinaryOperator {
|
|||
BiggerEquals
|
||||
}
|
||||
|
||||
export function nullSafeIsEquivalent<T extends{isEquivalent(other: T): boolean}>(
|
||||
base: T | null, other: T | null) {
|
||||
if (base == null || other == null) {
|
||||
return base == other;
|
||||
}
|
||||
return base.isEquivalent(other);
|
||||
}
|
||||
|
||||
export function areAllEquivalent<T extends{isEquivalent(other: T): boolean}>(
|
||||
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 = <BuiltinVar>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 = <BuiltinMethod>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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue