feat(compiler): reuse the TypeScript typecheck for template typechecking. (#19152)

This speeds up the compilation process significantly.

Also introduces a new option `fullTemplateTypeCheck` to do more checks in templates:
- check expressions inside of templatized content (e.g. inside of `<div *ngIf>`).
- check the arguments of calls to the `transform` function of pipes
- check references to directives that were exposed as variables via `exportAs`
PR Close #19152
This commit is contained in:
Tobias Bosch
2017-09-11 15:18:19 -07:00
committed by Matias Niemelä
parent 554fe65690
commit 996c7c2dde
22 changed files with 712 additions and 401 deletions

View File

@ -1,231 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, NgAnalyzedModules, ParseSourceSpan, Statement, StaticReflector, TypeScriptEmitter, createAotCompiler} from '@angular/compiler';
import * as ts from 'typescript';
import {DEFAULT_ERROR_CODE, Diagnostic, SOURCE} from '../transformers/api';
interface FactoryInfo {
source: ts.SourceFile;
context: EmitterVisitorContext;
}
type FactoryInfoMap = Map<string, FactoryInfo>;
const stubCancellationToken: ts.CancellationToken = {
isCancellationRequested(): boolean{return false;},
throwIfCancellationRequested(): void{}
};
export class TypeChecker {
private _aotCompiler: AotCompiler|undefined;
private _reflector: StaticReflector|undefined;
private _factories: Map<string, FactoryInfo>|undefined;
private _factoryNames: string[]|undefined;
private _diagnosticProgram: ts.Program|undefined;
private _diagnosticsByFile: Map<string, Diagnostic[]>|undefined;
private _currentCancellationToken: ts.CancellationToken = stubCancellationToken;
private _partial: boolean = false;
constructor(
private program: ts.Program, private tsOptions: ts.CompilerOptions,
private compilerHost: ts.CompilerHost, private aotCompilerHost: AotCompilerHost,
private aotOptions: AotCompilerOptions, private _analyzedModules?: NgAnalyzedModules,
private _generatedFiles?: GeneratedFile[]) {}
getDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken): Diagnostic[] {
this._currentCancellationToken = cancellationToken || stubCancellationToken;
try {
return fileName ?
this.diagnosticsByFileName.get(fileName) || [] :
([] as Diagnostic[]).concat(...Array.from(this.diagnosticsByFileName.values()));
} finally {
this._currentCancellationToken = stubCancellationToken;
}
}
get partialResults(): boolean { return this._partial; }
private get analyzedModules(): NgAnalyzedModules {
return this._analyzedModules || (this._analyzedModules = this.aotCompiler.analyzeModulesSync(
this.program.getSourceFiles().map(sf => sf.fileName)));
}
private get diagnosticsByFileName(): Map<string, Diagnostic[]> {
return this._diagnosticsByFile || this.createDiagnosticsByFile();
}
private get diagnosticProgram(): ts.Program {
return this._diagnosticProgram || this.createDiagnosticProgram();
}
private get generatedFiles(): GeneratedFile[] {
let result = this._generatedFiles;
if (!result) {
this._generatedFiles = result = this.aotCompiler.emitAllImpls(this.analyzedModules);
}
return result;
}
private get aotCompiler(): AotCompiler {
return this._aotCompiler || this.createCompilerAndReflector();
}
private get reflector(): StaticReflector {
let result = this._reflector;
if (!result) {
this.createCompilerAndReflector();
result = this._reflector !;
}
return result;
}
private get factories(): Map<string, FactoryInfo> {
return this._factories || this.createFactories();
}
private get factoryNames(): string[] {
return this._factoryNames || (this.createFactories() && this._factoryNames !);
}
private createCompilerAndReflector() {
const {compiler, reflector} = createAotCompiler(this.aotCompilerHost, this.aotOptions);
this._reflector = reflector;
return this._aotCompiler = compiler;
}
private createDiagnosticProgram() {
// Create a program that is all the files from the original program plus the factories.
const existingFiles = this.program.getSourceFiles().map(source => source.fileName);
const host = new TypeCheckingHost(this.compilerHost, this.program, this.factories);
return this._diagnosticProgram =
ts.createProgram([...existingFiles, ...this.factoryNames], this.tsOptions, host);
}
private createFactories() {
// Create all the factory files with enough information to map the diagnostics reported for the
// created file back to the original source.
const emitter = new TypeScriptEmitter();
const factorySources =
this.generatedFiles.filter(file => file.stmts != null && file.stmts.length)
.map<[string, FactoryInfo]>(
file => [file.genFileUrl, createFactoryInfo(emitter, file)]);
this._factories = new Map(factorySources);
this._factoryNames = Array.from(this._factories.keys());
return this._factories;
}
private createDiagnosticsByFile() {
// Collect all the diagnostics binned by original source file name.
const result = new Map<string, Diagnostic[]>();
const diagnosticsFor = (fileName: string) => {
let r = result.get(fileName);
if (!r) {
r = [];
result.set(fileName, r);
}
return r;
};
const program = this.diagnosticProgram;
for (const factoryName of this.factoryNames) {
if (this._currentCancellationToken.isCancellationRequested()) return result;
const sourceFile = program.getSourceFile(factoryName);
for (const diagnostic of this.diagnosticProgram.getSemanticDiagnostics(sourceFile)) {
if (diagnostic.file && diagnostic.start) {
const span = this.sourceSpanOf(diagnostic.file, diagnostic.start);
if (span) {
const fileName = span.start.file.url;
const diagnosticsList = diagnosticsFor(fileName);
diagnosticsList.push({
messageText: diagnosticMessageToString(diagnostic.messageText),
category: diagnostic.category, span,
source: SOURCE,
code: DEFAULT_ERROR_CODE
});
}
}
}
}
return result;
}
private sourceSpanOf(source: ts.SourceFile, start: number): ParseSourceSpan|null {
// Find the corresponding TypeScript node
const info = this.factories.get(source.fileName);
if (info) {
const {line, character} = ts.getLineAndCharacterOfPosition(source, start);
return info.context.spanOf(line, character);
}
return null;
}
}
function diagnosticMessageToString(message: ts.DiagnosticMessageChain | string): string {
return ts.flattenDiagnosticMessageText(message, '\n');
}
const REWRITE_PREFIX = /^\u0275[0-9]+$/;
function createFactoryInfo(emitter: TypeScriptEmitter, file: GeneratedFile): FactoryInfo {
const {sourceText, context} = emitter.emitStatementsAndContext(
file.srcFileUrl, file.genFileUrl, file.stmts !,
/* preamble */ undefined, /* emitSourceMaps */ undefined,
/* referenceFilter */ reference => !!(reference.name && REWRITE_PREFIX.test(reference.name)));
const source = ts.createSourceFile(
file.genFileUrl, sourceText, ts.ScriptTarget.Latest, /* setParentNodes */ true);
return {source, context};
}
class TypeCheckingHost implements ts.CompilerHost {
constructor(
private host: ts.CompilerHost, private originalProgram: ts.Program,
private factories: Map<string, FactoryInfo>) {}
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)): ts.SourceFile {
const originalSource = this.originalProgram.getSourceFile(fileName);
if (originalSource) {
return originalSource;
}
const factoryInfo = this.factories.get(fileName);
if (factoryInfo) {
return factoryInfo.source;
}
return this.host.getSourceFile(fileName, languageVersion, onError);
}
getDefaultLibFileName(options: ts.CompilerOptions): string {
return this.host.getDefaultLibFileName(options);
}
writeFile: ts.WriteFileCallback =
() => { throw new Error('Unexpected write in diagnostic program'); };
getCurrentDirectory(): string { return this.host.getCurrentDirectory(); }
getDirectories(path: string): string[] { return this.host.getDirectories(path); }
getCanonicalFileName(fileName: string): string {
return this.host.getCanonicalFileName(fileName);
}
useCaseSensitiveFileNames(): boolean { return this.host.useCaseSensitiveFileNames(); }
getNewLine(): string { return this.host.getNewLine(); }
fileExists(fileName: string): boolean {
return this.factories.has(fileName) || this.host.fileExists(fileName);
}
readFile(fileName: string): string {
const factoryInfo = this.factories.get(fileName);
return (factoryInfo && factoryInfo.source.text) || this.host.readFile(fileName);
}
}

View File

@ -0,0 +1,57 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ParseSourceSpan} from '@angular/compiler';
import * as ts from 'typescript';
import {DEFAULT_ERROR_CODE, Diagnostic, SOURCE} from '../transformers/api';
import {GENERATED_FILES} from '../transformers/util';
export interface TypeCheckHost {
ngSpanOf(fileName: string, line: number, character: number): ParseSourceSpan|null;
}
export function translateDiagnostics(host: TypeCheckHost, untranslatedDiagnostics: ts.Diagnostic[]):
{ts: ts.Diagnostic[], ng: Diagnostic[]} {
const ts: ts.Diagnostic[] = [];
const ng: Diagnostic[] = [];
untranslatedDiagnostics.forEach((diagnostic) => {
if (diagnostic.file && diagnostic.start && GENERATED_FILES.test(diagnostic.file.fileName)) {
// We need to filter out diagnostics about unused functions as
// they are in fact referenced by nobody and only serve to surface
// type check errors.
if (diagnostic.code === /* ... is declared but never used */ 6133) {
return;
}
const span = sourceSpanOf(host, diagnostic.file, diagnostic.start);
if (span) {
const fileName = span.start.file.url;
ng.push({
messageText: diagnosticMessageToString(diagnostic.messageText),
category: diagnostic.category, span,
source: SOURCE,
code: DEFAULT_ERROR_CODE
});
return;
}
}
ts.push(diagnostic);
});
return {ts, ng};
}
function sourceSpanOf(host: TypeCheckHost, source: ts.SourceFile, start: number): ParseSourceSpan|
null {
const {line, character} = ts.getLineAndCharacterOfPosition(source, start);
return host.ngSpanOf(source.fileName, line, character);
}
function diagnosticMessageToString(message: ts.DiagnosticMessageChain | string): string {
return ts.flattenDiagnosticMessageText(message, '\n');
}

View File

@ -15,7 +15,6 @@ to the language service.
*/
export {CompilerHost, CompilerHostContext, MetadataProvider, ModuleResolutionHostAdapter, NodeCompilerHostContext} from './compiler_host';
export {TypeChecker} from './diagnostics/check_types';
export {DiagnosticTemplateInfo, ExpressionDiagnostic, getExpressionDiagnostics, getExpressionScope, getTemplateExpressionDiagnostics} from './diagnostics/expression_diagnostics';
export {AstType, DiagnosticKind, ExpressionDiagnosticsContext, TypeDiagnostic} from './diagnostics/expression_type';
export {BuiltinType, DeclarationKind, Definition, Location, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './diagnostics/symbols';

View File

@ -18,10 +18,6 @@ const TS_EXT = /\.ts$/;
export type Diagnostics = Array<ts.Diagnostic|api.Diagnostic>;
function isTsDiagnostic(diagnostic: any): diagnostic is ts.Diagnostic {
return diagnostic && diagnostic.source != 'angular';
}
export function formatDiagnostics(options: api.CompilerOptions, diags: Diagnostics): string {
if (diags && diags.length) {
const tsFormatHost: ts.FormatDiagnosticsHost = {
@ -31,7 +27,7 @@ export function formatDiagnostics(options: api.CompilerOptions, diags: Diagnosti
};
return diags
.map(d => {
if (isTsDiagnostic(d)) {
if (api.isTsDiagnostic(d)) {
return ts.formatDiagnostics([d], tsFormatHost);
} else {
let res = ts.DiagnosticCategory[d.category];

View File

@ -21,6 +21,14 @@ export interface Diagnostic {
source: 'angular';
}
export function isTsDiagnostic(diagnostic: any): diagnostic is ts.Diagnostic {
return diagnostic != null && diagnostic.source !== 'angular';
}
export function isNgDiagnostic(diagnostic: any): diagnostic is Diagnostic {
return diagnostic != null && diagnostic.source === 'angular';
}
export interface CompilerOptions extends ts.CompilerOptions {
// Absolute path to a directory where generated file structure is written.
// If unspecified, generated files will be written alongside sources.
@ -73,6 +81,10 @@ export interface CompilerOptions extends ts.CompilerOptions {
// Default is true.
generateCodeForLibraries?: boolean;
// Whether to enable all type checks for templates.
// This will be true be default in Angular 6.
fullTemplateTypeCheck?: boolean;
// Insert JSDoc type annotations needed by Closure Compiler
annotateForClosureCompiler?: boolean;

View File

@ -6,20 +6,19 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompiler, AotCompilerHost, AotCompilerOptions, GeneratedFile, MessageBundle, NgAnalyzedModules, Serializer, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isSyntaxError, toTypeScript} from '@angular/compiler';
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, MessageBundle, NgAnalyzedModules, ParseSourceSpan, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isSyntaxError, toTypeScript} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {BaseAotCompilerHost} from '../compiler_host';
import {TypeChecker} from '../diagnostics/check_types';
import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics';
import {createBundleIndexHost} from '../metadata/index';
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, EmitFlags, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
const GENERATED_FILES = /(.*?)\.(ngfactory|shim\.ngstyle|ngstyle|ngsummary)\.(js|d\.ts|ts)$/;
import {GENERATED_FILES} from './util';
const emptyModules: NgAnalyzedModules = {
ngModules: [],
@ -45,12 +44,11 @@ class AngularCompilerProgram implements Program {
private _structuralDiagnostics: Diagnostic[] = [];
private _stubs: GeneratedFile[]|undefined;
private _stubFiles: string[]|undefined;
private _programWithStubsHost: ts.CompilerHost|undefined;
private _programWithStubsHost: ts.CompilerHost&TypeCheckHost|undefined;
private _programWithStubs: ts.Program|undefined;
private _generatedFiles: GeneratedFile[]|undefined;
private _generatedFileDiagnostics: Diagnostic[]|undefined;
private _typeChecker: TypeChecker|undefined;
private _semanticDiagnostics: Diagnostic[]|undefined;
private _semanticDiagnostics: {ts: ts.Diagnostic[], ng: Diagnostic[]}|undefined;
private _optionsDiagnostics: Diagnostic[] = [];
constructor(
@ -109,7 +107,7 @@ class AngularCompilerProgram implements Program {
getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
ts.Diagnostic[] {
return this.programWithStubs.getSemanticDiagnostics(sourceFile, cancellationToken);
return this.semanticDiagnostics.ts;
}
getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken):
@ -119,8 +117,7 @@ class AngularCompilerProgram implements Program {
// If we have diagnostics during the parser phase the type check phase is not meaningful so skip
// it.
if (compilerDiagnostics && compilerDiagnostics.length) return compilerDiagnostics;
return this.typeChecker.getDiagnostics(fileName, cancellationToken);
return this.semanticDiagnostics.ng;
}
loadNgStructureAsync(): Promise<void> {
@ -187,7 +184,7 @@ class AngularCompilerProgram implements Program {
}, []));
}
private get programWithStubsHost(): ts.CompilerHost {
private get programWithStubsHost(): ts.CompilerHost&TypeCheckHost {
return this._programWithStubsHost || (this._programWithStubsHost = createProgramWithStubsHost(
this.stubs, this.tsProgram, this.host));
}
@ -200,16 +197,15 @@ class AngularCompilerProgram implements Program {
return this._generatedFiles || (this._generatedFiles = this.generateFiles());
}
private get typeChecker(): TypeChecker {
return (this._typeChecker && !this._typeChecker.partialResults) ?
this._typeChecker :
(this._typeChecker = this.createTypeChecker());
}
private get generatedFileDiagnostics(): Diagnostic[]|undefined {
return this.generatedFiles && this._generatedFileDiagnostics !;
}
private get semanticDiagnostics(): {ts: ts.Diagnostic[], ng: Diagnostic[]} {
return this._semanticDiagnostics ||
(this._semanticDiagnostics = this.generateSemanticDiagnostics());
}
private calculateTransforms(customTransformers?: CustomTransformers): ts.CustomTransformers {
const beforeTs: ts.TransformerFactory<ts.SourceFile>[] = [];
if (!this.options.disableExpressionLowering) {
@ -283,12 +279,6 @@ class AngularCompilerProgram implements Program {
}
}
private createTypeChecker(): TypeChecker {
return new TypeChecker(
this.tsProgram, this.options, this.host, this.aotCompilerHost, this.options,
this.analyzedModules, this.generatedFiles);
}
private createProgramWithStubs(): ts.Program {
// If we are skipping code generation just use the original program.
// Otherwise, create a new program that includes the stub files.
@ -297,6 +287,11 @@ class AngularCompilerProgram implements Program {
ts.createProgram(
[...this.rootNames, ...this.stubFiles], this.options, this.programWithStubsHost);
}
private generateSemanticDiagnostics(): {ts: ts.Diagnostic[], ng: Diagnostic[]} {
return translateDiagnostics(
this.programWithStubsHost, this.programWithStubs.getSemanticDiagnostics());
}
}
class AotCompilerHostImpl extends BaseAotCompilerHost<CompilerHost> {
@ -360,6 +355,7 @@ function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions {
enableLegacyTemplate: options.enableLegacyTemplate,
enableSummariesForJit: true,
preserveWhitespaces: options.preserveWhitespaces,
fullTemplateTypeCheck: options.fullTemplateTypeCheck,
};
}
@ -453,13 +449,15 @@ function getNgOptionDiagnostics(options: CompilerOptions): Diagnostic[] {
function createProgramWithStubsHost(
generatedFiles: GeneratedFile[], originalProgram: ts.Program,
originalHost: ts.CompilerHost): ts.CompilerHost {
originalHost: ts.CompilerHost): ts.CompilerHost&TypeCheckHost {
interface FileData {
g: GeneratedFile;
s?: ts.SourceFile;
emitCtx?: EmitterVisitorContext;
}
return new class implements ts.CompilerHost {
return new class implements ts.CompilerHost, TypeCheckHost {
private generatedFiles: Map<string, FileData>;
private emitter = new TypeScriptEmitter();
writeFile: ts.WriteFileCallback;
getCancellationToken: () => ts.CancellationToken;
getDefaultLibLocation: () => string;
@ -487,13 +485,27 @@ function createProgramWithStubsHost(
this.trace = s => originalHost.trace !(s);
}
}
ngSpanOf(fileName: string, line: number, character: number): ParseSourceSpan|null {
const data = this.generatedFiles.get(fileName);
if (data && data.emitCtx) {
return data.emitCtx.spanOf(line, character);
}
return null;
}
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined): ts.SourceFile {
const data = this.generatedFiles.get(fileName);
if (data) {
return data.s || (data.s = ts.createSourceFile(
fileName, data.g.source || toTypeScript(data.g), languageVersion));
if (!data.s) {
const {sourceText, context} = this.emitter.emitStatementsAndContext(
data.g.srcFileUrl, data.g.genFileUrl, data.g.stmts !,
/* preamble */ undefined, /* emitSourceMaps */ undefined,
/* referenceFilter */ undefined);
data.emitCtx = context;
data.s = ts.createSourceFile(fileName, sourceText, languageVersion);
}
return data.s;
}
return originalProgram.getSourceFile(fileName) ||
originalHost.getSourceFile(fileName, languageVersion, onError);

View File

@ -0,0 +1,9 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export const GENERATED_FILES = /(.*?)\.(ngfactory|shim\.ngstyle|ngstyle|ngsummary)\.(js|d\.ts|ts)$/;