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)$/;

View File

@ -6,95 +6,200 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompilerOptions, createAotCompiler} from '@angular/compiler';
import {EmittingCompilerHost, MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, arrayToMockDir, arrayToMockMap, isSource, settings, setup, toMockFileArray} from '@angular/compiler/test/aot/test_util';
import * as ng from '@angular/compiler-cli';
import {makeTempDir} from '@angular/tsc-wrapped/test/test_support';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {TypeChecker} from '../../src/diagnostics/check_types';
import {Diagnostic} from '../../src/transformers/api';
import {LowerMetadataCache} from '../../src/transformers/lower_expressions';
function compile(
rootDirs: MockData, options: AotCompilerOptions = {},
tsOptions: ts.CompilerOptions = {}): Diagnostic[] {
const rootDirArr = toMockFileArray(rootDirs);
const scriptNames = rootDirArr.map(entry => entry.fileName).filter(isSource);
const host = new MockCompilerHost(scriptNames, arrayToMockDir(rootDirArr));
const aotHost = new MockAotCompilerHost(host, new LowerMetadataCache({}));
const tsSettings = {...settings, ...tsOptions};
const program = ts.createProgram(host.scriptNames.slice(0), tsSettings, host);
const ngChecker = new TypeChecker(program, tsSettings, host, aotHost, options);
return ngChecker.getDiagnostics();
function getNgRootDir() {
const moduleFilename = module.filename.replace(/\\/g, '/');
const distIndex = moduleFilename.indexOf('/dist/all');
return moduleFilename.substr(0, distIndex);
}
describe('ng type checker', () => {
let angularFiles = setup();
let basePath: string;
let write: (fileName: string, content: string) => void;
let errorSpy: jasmine.Spy&((s: string) => void);
function accept(...files: MockDirectory[]) {
expectNoDiagnostics(compile([angularFiles, QUICKSTART, ...files]));
function compileAndCheck(
mockDirs: {[fileName: string]: string}[],
overrideOptions: ng.CompilerOptions = {}): ng.Diagnostics {
const fileNames: string[] = [];
mockDirs.forEach((dir) => {
Object.keys(dir).forEach((fileName) => {
if (fileName.endsWith('.ts')) {
fileNames.push(path.resolve(basePath, fileName));
}
write(fileName, dir[fileName]);
});
});
const options: ng.CompilerOptions = {
basePath,
'experimentalDecorators': true,
'skipLibCheck': true,
'strict': true,
'types': [],
'outDir': path.resolve(basePath, 'built'),
'rootDir': basePath,
'baseUrl': basePath,
'declaration': true,
'target': ts.ScriptTarget.ES5,
'module': ts.ModuleKind.ES2015,
'moduleResolution': ts.ModuleResolutionKind.NodeJs,
'lib': [
path.resolve(basePath, 'node_modules/typescript/lib/lib.es6.d.ts'),
path.resolve(basePath, 'node_modules/typescript/lib/lib.dom.d.ts')
],
'typeRoots': [path.resolve(basePath, 'node_modules/@types')], ...overrideOptions
};
const {diagnostics} = ng.performCompilation({rootNames: fileNames, options});
return diagnostics;
}
function reject(message: string | RegExp, ...files: MockDirectory[]) {
const diagnostics = compile([angularFiles, QUICKSTART, ...files]);
beforeEach(() => {
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
basePath = makeTempDir();
write = (fileName: string, content: string) => {
const dir = path.dirname(fileName);
if (dir != '.') {
const newDir = path.join(basePath, dir);
if (!fs.existsSync(newDir)) fs.mkdirSync(newDir);
}
fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'});
};
const ngRootDir = getNgRootDir();
const nodeModulesPath = path.resolve(basePath, 'node_modules');
fs.mkdirSync(nodeModulesPath);
fs.symlinkSync(
path.resolve(ngRootDir, 'dist', 'all', '@angular'),
path.resolve(nodeModulesPath, '@angular'));
fs.symlinkSync(
path.resolve(ngRootDir, 'node_modules', 'rxjs'), path.resolve(nodeModulesPath, 'rxjs'));
fs.symlinkSync(
path.resolve(ngRootDir, 'node_modules', 'typescript'),
path.resolve(nodeModulesPath, 'typescript'));
});
function accept(
files: {[fileName: string]: string} = {}, overrideOptions: ng.CompilerOptions = {}) {
expectNoDiagnostics(compileAndCheck([QUICKSTART, files], overrideOptions));
}
function reject(
message: string | RegExp, location: RegExp, files: {[fileName: string]: string},
overrideOptions: ng.CompilerOptions = {}) {
const diagnostics = compileAndCheck([QUICKSTART, files], overrideOptions);
if (!diagnostics || !diagnostics.length) {
throw new Error('Expected a diagnostic erorr message');
} else {
const matches: (d: Diagnostic) => boolean = typeof message === 'string' ?
d => d.messageText == message :
d => message.test(d.messageText);
const matchingDiagnostics = diagnostics.filter(matches);
const matches: (d: ng.Diagnostic) => boolean = typeof message === 'string' ?
d => ng.isNgDiagnostic(d)&& d.messageText == message :
d => ng.isNgDiagnostic(d) && message.test(d.messageText);
const matchingDiagnostics = diagnostics.filter(matches) as ng.Diagnostic[];
if (!matchingDiagnostics || !matchingDiagnostics.length) {
throw new Error(
`Expected a diagnostics matching ${message}, received\n ${diagnostics.map(d => d.messageText).join('\n ')}`);
}
const span = matchingDiagnostics[0].span;
if (!span) {
throw new Error('Expected a sourceSpan');
}
expect(`${span.start.file.url}@${span.start.line}:${span.start.offset}`).toMatch(location);
}
}
it('should accept unmodified QuickStart', () => { accept(); });
describe('with modified quickstart', () => {
function a(template: string) {
accept({quickstart: {app: {'app.component.ts': appComponentSource(template)}}});
it('should accept unmodified QuickStart with tests for unused variables', () => {
accept({}, {
strict: true,
noUnusedLocals: true,
noUnusedParameters: true,
});
});
describe('with modified quickstart (fullTemplateTypeCheck: false)', () => {
addTests({fullTemplateTypeCheck: false});
});
describe('with modified quickstart (fullTemplateTypeCheck: true)', () => {
addTests({fullTemplateTypeCheck: true});
});
function addTests(config: {fullTemplateTypeCheck: boolean}) {
function a(template: string) { accept({'src/app.component.html': template}, config); }
function r(template: string, message: string | RegExp, location: string) {
reject(
message, new RegExp(`app\.component\.html\@${location}$`),
{'src/app.component.html': template}, config);
}
function r(template: string, message: string | RegExp) {
reject(message, {quickstart: {app: {'app.component.ts': appComponentSource(template)}}});
function rejectOnlyWithFullTemplateTypeCheck(
template: string, message: string | RegExp, location: string) {
if (config.fullTemplateTypeCheck) {
r(template, message, location);
} else {
a(template);
}
}
it('should report an invalid field access',
() => { r('{{fame}}', `Property 'fame' does not exist on type 'AppComponent'.`); });
it('should report an invalid field access', () => {
r('<div>{{fame}}<div>', `Property 'fame' does not exist on type 'AppComponent'.`, '0:5');
});
it('should reject a reference to a field of a nullable',
() => { r('{{maybePerson.name}}', `Object is possibly 'undefined'.`); });
() => { r('<div>{{maybePerson.name}}</div>', `Object is possibly 'undefined'.`, '0:5'); });
it('should accept a reference to a field of a nullable using using non-null-assert',
() => { a('{{maybePerson!.name}}'); });
it('should accept a safe property access of a nullable person',
() => { a('{{maybePerson?.name}}'); });
it('should accept a function call', () => { a('{{getName()}}'); });
it('should reject an invalid method', () => {
r('{{getFame()}}',
`Property 'getFame' does not exist on type 'AppComponent'. Did you mean 'getName'?`);
r('<div>{{getFame()}}</div>',
`Property 'getFame' does not exist on type 'AppComponent'. Did you mean 'getName'?`, '0:5');
});
it('should accept a field access of a method result', () => { a('{{getPerson().name}}'); });
it('should reject an invalid field reference of a method result',
() => { r('{{getPerson().fame}}', `Property 'fame' does not exist on type 'Person'.`); });
it('should reject an access to a nullable field of a method result',
() => { r('{{getMaybePerson().name}}', `Object is possibly 'undefined'.`); });
it('should reject an invalid field reference of a method result', () => {
r('<div>{{getPerson().fame}}</div>', `Property 'fame' does not exist on type 'Person'.`,
'0:5');
});
it('should reject an access to a nullable field of a method result', () => {
r('<div>{{getMaybePerson().name}}</div>', `Object is possibly 'undefined'.`, '0:5');
});
it('should accept a nullable assert of a nullable field refernces of a method result',
() => { a('{{getMaybePerson()!.name}}'); });
it('should accept a safe property access of a nullable field reference of a method result',
() => { a('{{getMaybePerson()?.name}}'); });
it('should report an invalid field access inside of an ng-template', () => {
rejectOnlyWithFullTemplateTypeCheck(
'<ng-template>{{fame}}</ng-template>',
`Property 'fame' does not exist on type 'AppComponent'.`, '0:13');
});
it('should report an invalid call to a pipe', () => {
rejectOnlyWithFullTemplateTypeCheck(
'<div>{{"hello" | aPipe}}</div>',
`Argument of type '"hello"' is not assignable to parameter of type 'number'.`, '0:5');
});
it('should report an invalid property on an exportAs directive', () => {
rejectOnlyWithFullTemplateTypeCheck(
'<div aDir #aDir="aDir">{{aDir.fname}}</div>',
`Property 'fname' does not exist on type 'ADirective'. Did you mean 'name'?`, '0:23');
});
}
describe('with lowered expressions', () => {
it('should not report lowered expressions as errors', () => {
expectNoDiagnostics(compile([angularFiles, LOWERING_QUICKSTART]));
});
it('should not report lowered expressions as errors',
() => { expectNoDiagnostics(compileAndCheck([LOWERING_QUICKSTART])); });
});
});
function appComponentSource(template: string): string {
function appComponentSource(): string {
return `
import {Component} from '@angular/core';
import {Component, Pipe, Directive} from '@angular/core';
export interface Person {
name: string;
@ -109,7 +214,7 @@ function appComponentSource(template: string): string {
}
@Component({
template: '${template}'
templateUrl: './app.component.html'
})
export class AppComponent {
name = 'Angular';
@ -119,40 +224,48 @@ function appComponentSource(template: string): string {
getName(): string { return this.name; }
getPerson(): Person { return this.person; }
getMaybePerson(): Person | undefined { this.maybePerson; }
getMaybePerson(): Person | undefined { return this.maybePerson; }
}
@Pipe({
name: 'aPipe',
})
export class APipe {
transform(n: number): number { return n + 1; }
}
@Directive({
selector: '[aDir]',
exportAs: 'aDir'
})
export class ADirective {
name = 'ADirective';
}
`;
}
const QUICKSTART: MockDirectory = {
quickstart: {
app: {
'app.component.ts': appComponentSource('<h1>Hello {{name}}</h1>'),
'app.module.ts': `
const QUICKSTART = {
'src/app.component.ts': appComponentSource(),
'src/app.component.html': '<h1>Hello {{name}}</h1>',
'src/app.module.ts': `
import { NgModule } from '@angular/core';
import { toString } from './utils';
import { AppComponent } from './app.component';
import { AppComponent, APipe, ADirective } from './app.component';
@NgModule({
declarations: [ AppComponent ],
declarations: [ AppComponent, APipe, ADirective ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
`
}
}
};
const LOWERING_QUICKSTART: MockDirectory = {
quickstart: {
app: {
'app.component.ts': appComponentSource('<h1>Hello {{name}}</h1>'),
'app.module.ts': `
const LOWERING_QUICKSTART = {
'src/app.component.ts': appComponentSource(),
'src/app.component.html': '<h1>Hello {{name}}</h1>',
'src/app.module.ts': `
import { NgModule, Component } from '@angular/core';
import { toString } from './utils';
import { AppComponent } from './app.component';
import { AppComponent, APipe, ADirective } from './app.component';
class Foo {}
@ -165,17 +278,15 @@ const LOWERING_QUICKSTART: MockDirectory = {
export class Bar {}
@NgModule({
declarations: [ AppComponent, Bar ],
declarations: [ AppComponent, APipe, ADirective, Bar ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
`
}
}
};
function expectNoDiagnostics(diagnostics: Diagnostic[]) {
function expectNoDiagnostics(diagnostics: ng.Diagnostics) {
if (diagnostics && diagnostics.length) {
throw new Error(diagnostics.map(d => `${d.span}: ${d.messageText}`).join('\n'));
throw new Error(ng.formatDiagnostics({}, diagnostics));
}
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, createHostComponentMeta, flatten, identifierName, sourceUrl, templateSourceUrl} from '../compile_metadata';
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, createHostComponentMeta, flatten, identifierName, sourceUrl, templateSourceUrl} from '../compile_metadata';
import {CompilerConfig} from '../config';
import {MessageBundle} from '../i18n/message_bundle';
import {Identifiers, createTokenForExternalReference} from '../identifiers';
@ -19,8 +19,10 @@ import * as o from '../output/output_ast';
import {ParseError} from '../parse_util';
import {CompiledStylesheet, StyleCompiler} from '../style_compiler';
import {SummaryResolver} from '../summary_resolver';
import {TemplateAst} from '../template_parser/template_ast';
import {TemplateParser} from '../template_parser/template_parser';
import {OutputContext, syntaxError} from '../util';
import {TypeCheckCompiler} from '../view_compiler/type_check_compiler';
import {ViewCompileResult, ViewCompiler} from '../view_compiler/view_compiler';
import {AotCompilerHost} from './compiler_host';
@ -32,11 +34,15 @@ import {createForJitStub, serializeSummaries} from './summary_serializer';
import {ngfactoryFilePath, splitTypescriptSuffix, summaryFileName, summaryForJitFileName, summaryForJitName} from './util';
export class AotCompiler {
private _templateAstCache =
new Map<StaticSymbol, {template: TemplateAst[], pipes: CompilePipeSummary[]}>();
constructor(
private _config: CompilerConfig, private _host: AotCompilerHost,
private _reflector: StaticReflector, private _metadataResolver: CompileMetadataResolver,
private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler,
private _viewCompiler: ViewCompiler, private _ngModuleCompiler: NgModuleCompiler,
private _htmlParser: HtmlParser, private _templateParser: TemplateParser,
private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler,
private _typeCheckCompiler: TypeCheckCompiler, private _ngModuleCompiler: NgModuleCompiler,
private _outputEmitter: OutputEmitter,
private _summaryResolver: SummaryResolver<StaticSymbol>, private _localeId: string|null,
private _translationFormat: string|null, private _enableSummariesForJit: boolean|null,
@ -66,9 +72,10 @@ export class AotCompiler {
}
emitAllStubs(analyzeResult: NgAnalyzedModules): GeneratedFile[] {
const {files} = analyzeResult;
const {files, ngModuleByPipeOrDirective} = analyzeResult;
const sourceModules = files.map(
file => this._compileStubFile(file.srcUrl, file.directives, file.pipes, file.ngModules));
file => this._compileStubFile(
file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.pipes, file.ngModules));
return flatten(sourceModules);
}
@ -112,7 +119,8 @@ export class AotCompiler {
}
private _compileStubFile(
srcFileUrl: string, directives: StaticSymbol[], pipes: StaticSymbol[],
srcFileUrl: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
directives: StaticSymbol[], pipes: StaticSymbol[],
ngModules: StaticSymbol[]): GeneratedFile[] {
const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1];
const generatedFiles: GeneratedFile[] = [];
@ -130,10 +138,17 @@ export class AotCompiler {
// the generated code)
directives.forEach((dirType) => {
const compMeta = this._metadataResolver.getDirectiveMetadata(<any>dirType);
if (!compMeta.isComponent) {
return;
}
const ngModule = ngModuleByPipeOrDirective.get(dirType);
if (!ngModule) {
throw new Error(
`Internal Error: cannot determine the module for component ${identifierName(compMeta.type)}!`);
}
this._compileComponentTypeCheckBlock(
ngFactoryOutputCtx, compMeta, ngModule, ngModule.transitiveModule.directives);
// Note: compMeta is a component and therefore template is non null.
compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => {
const styleContext = this._createOutputContext(_stylesModuleUrl(
@ -285,7 +300,8 @@ export class AotCompiler {
ngModule: CompileNgModuleMetadata, fileSuffix: string): void {
const hostType = this._metadataResolver.getHostComponentType(compMeta.type.reference);
const hostMeta = createHostComponentMeta(
hostType, compMeta, this._metadataResolver.getHostComponentViewClass(hostType));
hostType, compMeta, this._metadataResolver.getHostComponentViewClass(hostType),
this._htmlParser);
const hostViewFactoryVar =
this._compileComponent(outputCtx, hostMeta, ngModule, [compMeta.type], null, fileSuffix)
.viewClassVar;
@ -320,19 +336,32 @@ export class AotCompiler {
[o.StmtModifier.Final, o.StmtModifier.Exported]));
}
private _compileComponent(
outputCtx: OutputContext, compMeta: CompileDirectiveMetadata,
ngModule: CompileNgModuleMetadata, directiveIdentifiers: CompileIdentifierMetadata[],
componentStyles: CompiledStylesheet|null, fileSuffix: string): ViewCompileResult {
private _parseTemplate(
compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata,
directiveIdentifiers: CompileIdentifierMetadata[]):
{template: TemplateAst[], pipes: CompilePipeSummary[]} {
let result = this._templateAstCache.get(compMeta.type.reference);
if (result) {
return result;
}
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
const directives =
directiveIdentifiers.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference));
const pipes = ngModule.transitiveModule.pipes.map(
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
compMeta, compMeta.template !.template !, directives, pipes, ngModule.schemas,
result = this._templateParser.parse(
compMeta, compMeta.template !.htmlAst !, directives, pipes, ngModule.schemas,
templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces);
this._templateAstCache.set(compMeta.type.reference, result);
return result;
}
private _compileComponent(
outputCtx: OutputContext, compMeta: CompileDirectiveMetadata,
ngModule: CompileNgModuleMetadata, directiveIdentifiers: CompileIdentifierMetadata[],
componentStyles: CompiledStylesheet|null, fileSuffix: string): ViewCompileResult {
const {template: parsedTemplate, pipes: usedPipes} =
this._parseTemplate(compMeta, ngModule, directiveIdentifiers);
const stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]);
const viewResult = this._viewCompiler.compileComponent(
outputCtx, compMeta, parsedTemplate, stylesExpr, usedPipes);
@ -344,6 +373,14 @@ export class AotCompiler {
return viewResult;
}
private _compileComponentTypeCheckBlock(
outputCtx: OutputContext, compMeta: CompileDirectiveMetadata,
ngModule: CompileNgModuleMetadata, directiveIdentifiers: CompileIdentifierMetadata[]) {
const {template: parsedTemplate, pipes: usedPipes} =
this._parseTemplate(compMeta, ngModule, directiveIdentifiers);
this._typeCheckCompiler.compileComponent(outputCtx, compMeta, parsedTemplate, usedPipes);
}
private _createOutputContext(genFilePath: string): OutputContext {
const importExpr = (symbol: StaticSymbol, typeParams: o.Type[] | null = null) => {
if (!(symbol instanceof StaticSymbol)) {

View File

@ -24,6 +24,7 @@ import {StyleCompiler} from '../style_compiler';
import {TemplateParser} from '../template_parser/template_parser';
import {UrlResolver} from '../url_resolver';
import {syntaxError} from '../util';
import {TypeCheckCompiler} from '../view_compiler/type_check_compiler';
import {ViewCompiler} from '../view_compiler/view_compiler';
import {AotCompiler} from './compiler';
@ -81,9 +82,11 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom
console, symbolCache, staticReflector);
// TODO(vicb): do not pass options.i18nFormat here
const viewCompiler = new ViewCompiler(config, staticReflector, elementSchemaRegistry);
const typeCheckCompiler = new TypeCheckCompiler(options, staticReflector);
const compiler = new AotCompiler(
config, compilerHost, staticReflector, resolver, tmplParser, new StyleCompiler(urlResolver),
viewCompiler, new NgModuleCompiler(staticReflector), new TypeScriptEmitter(), summaryResolver,
config, compilerHost, staticReflector, resolver, htmlParser, tmplParser,
new StyleCompiler(urlResolver), viewCompiler, typeCheckCompiler,
new NgModuleCompiler(staticReflector), new TypeScriptEmitter(), summaryResolver,
options.locale || null, options.i18nFormat || null, options.enableSummariesForJit || null,
symbolResolver);
return {compiler, reflector: staticReflector};

View File

@ -16,4 +16,5 @@ export interface AotCompilerOptions {
enableLegacyTemplate?: boolean;
enableSummariesForJit?: boolean;
preserveWhitespaces?: boolean;
fullTemplateTypeCheck?: boolean;
}

View File

@ -9,6 +9,9 @@
import {StaticSymbol} from './aot/static_symbol';
import {ChangeDetectionStrategy, SchemaMetadata, Type, ViewEncapsulation} from './core';
import {LifecycleHooks} from './lifecycle_reflector';
import * as html from './ml_parser/ast';
import {HtmlParser} from './ml_parser/html_parser';
import {ParseTreeResult as HtmlParseTreeResult} from './ml_parser/parser';
import {CssSelector} from './selector';
import {splitAtColon, stringify} from './util';
@ -244,6 +247,7 @@ export class CompileTemplateMetadata {
encapsulation: ViewEncapsulation|null;
template: string|null;
templateUrl: string|null;
htmlAst: HtmlParseTreeResult|null;
isInline: boolean;
styles: string[];
styleUrls: string[];
@ -252,11 +256,13 @@ export class CompileTemplateMetadata {
ngContentSelectors: string[];
interpolation: [string, string]|null;
preserveWhitespaces: boolean;
constructor({encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets,
animations, ngContentSelectors, interpolation, isInline, preserveWhitespaces}: {
constructor({encapsulation, template, templateUrl, htmlAst, styles, styleUrls,
externalStylesheets, animations, ngContentSelectors, interpolation, isInline,
preserveWhitespaces}: {
encapsulation: ViewEncapsulation | null,
template: string|null,
templateUrl: string|null,
htmlAst: HtmlParseTreeResult|null,
styles: string[],
styleUrls: string[],
externalStylesheets: CompileStylesheetMetadata[],
@ -269,6 +275,7 @@ export class CompileTemplateMetadata {
this.encapsulation = encapsulation;
this.template = template;
this.templateUrl = templateUrl;
this.htmlAst = htmlAst;
this.styles = _normalizeArray(styles);
this.styleUrls = _normalizeArray(styleUrls);
this.externalStylesheets = _normalizeArray(externalStylesheets);
@ -503,15 +510,18 @@ export class CompileDirectiveMetadata {
*/
export function createHostComponentMeta(
hostTypeReference: any, compMeta: CompileDirectiveMetadata,
hostViewType: StaticSymbol | ProxyClass): CompileDirectiveMetadata {
hostViewType: StaticSymbol | ProxyClass, htmlParser: HtmlParser): CompileDirectiveMetadata {
const template = CssSelector.parse(compMeta.selector !)[0].getMatchingElementTemplate();
const templateUrl = '';
const htmlAst = htmlParser.parse(template, templateUrl);
return CompileDirectiveMetadata.create({
isHost: true,
type: {reference: hostTypeReference, diDeps: [], lifecycleHooks: []},
template: new CompileTemplateMetadata({
encapsulation: ViewEncapsulation.None,
template: template,
templateUrl: '',
template,
templateUrl,
htmlAst,
styles: [],
styleUrls: [],
ngContentSelectors: [],

View File

@ -30,7 +30,8 @@ export class CompilerConfig {
jitDevMode?: boolean,
missingTranslation?: MissingTranslationStrategy,
enableLegacyTemplate?: boolean,
preserveWhitespaces?: boolean
preserveWhitespaces?: boolean,
fullTemplateTypeCheck?: boolean
} = {}) {
this.defaultEncapsulation = defaultEncapsulation;
this.useJit = !!useJit;

View File

@ -150,7 +150,8 @@ export class DirectiveNormalizer {
return new CompileTemplateMetadata({
encapsulation,
template,
templateUrl: templateAbsUrl, styles, styleUrls,
templateUrl: templateAbsUrl,
htmlAst: rootNodesAndErrors, styles, styleUrls,
ngContentSelectors: visitor.ngContentSelectors,
animations: prenormData.animations,
interpolation: prenormData.interpolation, isInline,
@ -168,6 +169,7 @@ export class DirectiveNormalizer {
encapsulation: templateMeta.encapsulation,
template: templateMeta.template,
templateUrl: templateMeta.templateUrl,
htmlAst: templateMeta.htmlAst,
styles: templateMeta.styles,
styleUrls: templateMeta.styleUrls,
externalStylesheets: externalStylesheets,

View File

@ -11,6 +11,7 @@ import {CompileReflector} from '../compile_reflector';
import {CompilerConfig} from '../config';
import {Type} from '../core';
import {CompileMetadataResolver} from '../metadata_resolver';
import {HtmlParser} from '../ml_parser/html_parser';
import {NgModuleCompiler} from '../ng_module_compiler';
import * as ir from '../output/output_ast';
import {interpretStatements} from '../output/output_interpreter';
@ -43,11 +44,11 @@ export class JitCompiler {
private _sharedStylesheetCount = 0;
constructor(
private _metadataResolver: CompileMetadataResolver, private _templateParser: TemplateParser,
private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler,
private _ngModuleCompiler: NgModuleCompiler, private _summaryResolver: SummaryResolver<Type>,
private _reflector: CompileReflector, private _compilerConfig: CompilerConfig,
private _console: Console,
private _metadataResolver: CompileMetadataResolver, private _htmlParser: HtmlParser,
private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler,
private _viewCompiler: ViewCompiler, private _ngModuleCompiler: NgModuleCompiler,
private _summaryResolver: SummaryResolver<Type>, private _reflector: CompileReflector,
private _compilerConfig: CompilerConfig, private _console: Console,
private getExtraNgModuleProviders: (ngModule: any) => CompileProviderMetadata[]) {}
compileModuleSync(moduleType: Type): object {
@ -228,7 +229,7 @@ export class JitCompiler {
const hostClass = this._metadataResolver.getHostComponentType(compType);
const hostMeta = createHostComponentMeta(
hostClass, compMeta, (compMeta.componentFactory as any).viewDefFactory);
hostClass, compMeta, (compMeta.componentFactory as any).viewDefFactory, this._htmlParser);
compiledTemplate =
new CompiledTemplate(true, compMeta.type, hostMeta, ngModule, [compMeta.type]);
this._compiledHostTemplateCache.set(compType, compiledTemplate);

View File

@ -261,6 +261,7 @@ export class CompileMetadataResolver {
encapsulation: noUndefined(compMeta.encapsulation),
template: noUndefined(compMeta.template),
templateUrl: noUndefined(compMeta.templateUrl),
htmlAst: null,
styles: compMeta.styles || [],
styleUrls: compMeta.styleUrls || [],
animations: animations || [],

View File

@ -1141,8 +1141,8 @@ export function importType(
}
export function expressionType(
expr: Expression, typeModifiers: TypeModifier[] | null = null): ExpressionType|null {
return expr != null ? new ExpressionType(expr, typeModifiers) ! : null;
expr: Expression, typeModifiers: TypeModifier[] | null = null): ExpressionType {
return new ExpressionType(expr, typeModifiers);
}
export function literalArr(

View File

@ -101,8 +101,9 @@ export class TemplateParser {
public transforms: TemplateAstVisitor[]) {}
parse(
component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[],
pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string,
component: CompileDirectiveMetadata, template: string|ParseTreeResult,
directives: CompileDirectiveSummary[], pipes: CompilePipeSummary[], schemas: SchemaMetadata[],
templateUrl: string,
preserveWhitespaces: boolean): {template: TemplateAst[], pipes: CompilePipeSummary[]} {
const result = this.tryParse(
component, template, directives, pipes, schemas, templateUrl, preserveWhitespaces);
@ -126,11 +127,13 @@ export class TemplateParser {
}
tryParse(
component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[],
pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string,
preserveWhitespaces: boolean): TemplateParseResult {
let htmlParseResult = this._htmlParser !.parse(
template, templateUrl, true, this.getInterpolationConfig(component));
component: CompileDirectiveMetadata, template: string|ParseTreeResult,
directives: CompileDirectiveSummary[], pipes: CompilePipeSummary[], schemas: SchemaMetadata[],
templateUrl: string, preserveWhitespaces: boolean): TemplateParseResult {
let htmlParseResult = typeof template === 'string' ?
this._htmlParser !.parse(
template, templateUrl, true, this.getInterpolationConfig(component)) :
template;
if (!preserveWhitespaces) {
htmlParseResult = removeWhitespaces(htmlParseResult);

View File

@ -0,0 +1,283 @@
/**
* @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 {AotCompilerOptions} from '../aot/compiler_options';
import {StaticReflector} from '../aot/static_reflector';
import {StaticSymbol} from '../aot/static_symbol';
import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompilePipeSummary, viewClassName} from '../compile_metadata';
import {BuiltinConverter, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter';
import {AST, ASTWithSource, Interpolation} from '../expression_parser/ast';
import {Identifiers} from '../identifiers';
import * as o from '../output/output_ast';
import {convertValueToOutputAst} from '../output/value_util';
import {ParseSourceSpan} from '../parse_util';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, ProviderAstType, QueryMatch, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast';
import {OutputContext} from '../util';
/**
* Generates code that is used to type check templates.
*/
export class TypeCheckCompiler {
constructor(private options: AotCompilerOptions, private reflector: StaticReflector) {}
compileComponent(
outputCtx: OutputContext, component: CompileDirectiveMetadata, template: TemplateAst[],
usedPipes: CompilePipeSummary[]): void {
const pipes = new Map<string, StaticSymbol>();
usedPipes.forEach(p => pipes.set(p.name, p.type.reference));
let embeddedViewCount = 0;
const viewBuilderFactory = (parent: ViewBuilder | null): ViewBuilder => {
const embeddedViewIndex = embeddedViewCount++;
return new ViewBuilder(
this.options, this.reflector, outputCtx, parent, component.type.reference,
embeddedViewIndex, pipes, viewBuilderFactory);
};
const visitor = viewBuilderFactory(null);
visitor.visitAll([], template);
outputCtx.statements.push(...visitor.build());
}
}
interface ViewBuilderFactory {
(parent: ViewBuilder): ViewBuilder;
}
// Note: This is used as key in Map and should therefore be
// unique per value.
type OutputVarType = o.BuiltinTypeName | StaticSymbol;
interface Expression {
context: OutputVarType;
sourceSpan: ParseSourceSpan;
value: AST;
}
class ViewBuilder implements TemplateAstVisitor, LocalResolver {
private outputVarTypes = new Map<string, OutputVarType>();
private outputVarNames = new Map<OutputVarType, string>();
private refOutputVars = new Map<string, OutputVarType>();
private variables: VariableAst[] = [];
private children: ViewBuilder[] = [];
private updates: Expression[] = [];
private actions: Expression[] = [];
constructor(
private options: AotCompilerOptions, private reflector: StaticReflector,
private outputCtx: OutputContext, private parent: ViewBuilder|null,
private component: StaticSymbol, private embeddedViewIndex: number,
private pipes: Map<string, StaticSymbol>, private viewBuilderFactory: ViewBuilderFactory) {}
private getOrAddOutputVar(type: o.BuiltinTypeName|StaticSymbol): string {
let varName = this.outputVarNames.get(type);
if (!varName) {
varName = `_v${this.outputVarNames.size}`;
this.outputVarNames.set(type, varName);
this.outputVarTypes.set(varName, type);
}
return varName;
}
visitAll(variables: VariableAst[], astNodes: TemplateAst[]) {
this.variables = variables;
templateVisitAll(this, astNodes);
}
build(targetStatements: o.Statement[] = []): o.Statement[] {
this.children.forEach((child) => child.build(targetStatements));
const viewStmts: o.Statement[] = [];
let bindingCount = 0;
this.updates.forEach((expression) => {
const {sourceSpan, context, value} = this.preprocessUpdateExpression(expression);
const bindingId = `${bindingCount++}`;
const nameResolver = context === this.component ? this : null;
const {stmts, currValExpr} = convertPropertyBinding(
nameResolver, o.variable(this.getOrAddOutputVar(context)), value, bindingId);
stmts.push(new o.ExpressionStatement(currValExpr));
viewStmts.push(...stmts.map(
(stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan)));
});
this.actions.forEach(({sourceSpan, context, value}) => {
const bindingId = `${bindingCount++}`;
const nameResolver = context === this.component ? this : null;
const {stmts} = convertActionBinding(
nameResolver, o.variable(this.getOrAddOutputVar(context)), value, bindingId);
viewStmts.push(...stmts.map(
(stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan)));
});
const viewName = `_View_${this.component.name}_${this.embeddedViewIndex}`;
const params: o.FnParam[] = [];
this.outputVarNames.forEach((varName, varType) => {
const outputType = varType instanceof StaticSymbol ?
o.expressionType(this.outputCtx.importExpr(varType)) :
new o.BuiltinType(varType);
params.push(new o.FnParam(varName, outputType));
});
const viewFactory = new o.DeclareFunctionStmt(viewName, params, viewStmts);
targetStatements.push(viewFactory);
return targetStatements;
}
visitBoundText(ast: BoundTextAst, context: any): any {
const astWithSource = <ASTWithSource>ast.value;
const inter = <Interpolation>astWithSource.ast;
inter.expressions.forEach(
(expr) =>
this.updates.push({context: this.component, value: expr, sourceSpan: ast.sourceSpan}));
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
this.visitElementOrTemplate(ast);
// Note: The old view compiler used to use an `any` type
// for the context in any embedded view.
// We keep this behaivor behind a flag for now.
if (this.options.fullTemplateTypeCheck) {
const childVisitor = this.viewBuilderFactory(this);
this.children.push(childVisitor);
childVisitor.visitAll(ast.variables, ast.children);
}
}
visitElement(ast: ElementAst, context: any): any {
this.visitElementOrTemplate(ast);
let inputDefs: o.Expression[] = [];
let updateRendererExpressions: Expression[] = [];
let outputDefs: o.Expression[] = [];
ast.inputs.forEach((inputAst) => {
this.updates.push(
{context: this.component, value: inputAst.value, sourceSpan: inputAst.sourceSpan});
});
templateVisitAll(this, ast.children);
}
private visitElementOrTemplate(ast: {
outputs: BoundEventAst[],
directives: DirectiveAst[],
references: ReferenceAst[],
}) {
ast.directives.forEach((dirAst) => { this.visitDirective(dirAst); });
ast.references.forEach((ref) => {
let outputVarType: OutputVarType = null !;
// Note: The old view compiler used to use an `any` type
// for directives exposed via `exportAs`.
// We keep this behaivor behind a flag for now.
if (ref.value && ref.value.identifier && this.options.fullTemplateTypeCheck) {
outputVarType = ref.value.identifier.reference;
} else {
outputVarType = o.BuiltinTypeName.Dynamic;
}
this.refOutputVars.set(ref.name, outputVarType);
});
ast.outputs.forEach((outputAst) => {
this.actions.push(
{context: this.component, value: outputAst.handler, sourceSpan: outputAst.sourceSpan});
});
}
visitDirective(dirAst: DirectiveAst) {
const dirType = dirAst.directive.type.reference;
dirAst.inputs.forEach(
(input) => this.updates.push(
{context: this.component, value: input.value, sourceSpan: input.sourceSpan}));
// Note: The old view compiler used to use an `any` type
// for expressions in host properties / events.
// We keep this behaivor behind a flag for now.
if (this.options.fullTemplateTypeCheck) {
dirAst.hostProperties.forEach(
(inputAst) => this.updates.push(
{context: dirType, value: inputAst.value, sourceSpan: inputAst.sourceSpan}));
dirAst.hostEvents.forEach((hostEventAst) => this.actions.push({
context: dirType,
value: hostEventAst.handler,
sourceSpan: hostEventAst.sourceSpan
}));
}
}
getLocal(name: string): o.Expression|null {
if (name == EventHandlerVars.event.name) {
return o.variable(this.getOrAddOutputVar(o.BuiltinTypeName.Dynamic));
}
for (let currBuilder: ViewBuilder|null = this; currBuilder; currBuilder = currBuilder.parent) {
let outputVarType: OutputVarType|undefined;
// check references
outputVarType = currBuilder.refOutputVars.get(name);
if (outputVarType == null) {
// check variables
const varAst = currBuilder.variables.find((varAst) => varAst.name === name);
if (varAst) {
outputVarType = o.BuiltinTypeName.Dynamic;
}
}
if (outputVarType != null) {
return o.variable(this.getOrAddOutputVar(outputVarType));
}
}
return null;
}
private pipeOutputVar(name: string): string {
const pipe = this.pipes.get(name);
if (!pipe) {
throw new Error(
`Illegal State: Could not find pipe ${name} in template of ${this.component}`);
}
return this.getOrAddOutputVar(pipe);
}
private preprocessUpdateExpression(expression: Expression): Expression {
return {
sourceSpan: expression.sourceSpan,
context: expression.context,
value: convertPropertyBindingBuiltins(
{
createLiteralArrayConverter: (argCount: number) => (args: o.Expression[]) =>
o.literalArr(args),
createLiteralMapConverter: (keys: {key: string, quoted: boolean}[]) =>
(values: o.Expression[]) => {
const entries = keys.map((k, i) => ({
key: k.key,
value: values[i],
quoted: k.quoted,
}));
return o.literalMap(entries);
},
createPipeConverter: (name: string, argCount: number) => (args: o.Expression[]) => {
// Note: The old view compiler used to use an `any` type
// for pipe calls.
// We keep this behaivor behind a flag for now.
if (this.options.fullTemplateTypeCheck) {
return o.variable(this.pipeOutputVar(name)).callMethod('transform', args);
} else {
return o.variable(this.getOrAddOutputVar(o.BuiltinTypeName.Dynamic));
}
},
},
expression.value)
};
}
visitNgContent(ast: NgContentAst, context: any): any {}
visitText(ast: TextAst, context: any): any {}
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {}
visitReference(ast: ReferenceAst, context: any): any {}
visitVariable(ast: VariableAst, context: any): any {}
visitEvent(ast: BoundEventAst, context: any): any {}
visitElementProperty(ast: BoundElementPropertyAst, context: any): any {}
visitAttr(ast: AttrAst, context: any): any {}
}

View File

@ -92,6 +92,7 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
encapsulation: encapsulation || null,
template: template || null,
templateUrl: templateUrl || null,
htmlAst: null,
styles: styles || [],
styleUrls: styleUrls || [],
externalStylesheets: externalStylesheets || [],

View File

@ -101,6 +101,7 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
encapsulation: noUndefined(encapsulation),
template: noUndefined(template),
templateUrl: noUndefined(templateUrl),
htmlAst: null,
styles: styles || [],
styleUrls: styleUrls || [],
externalStylesheets: externalStylesheets || [],
@ -372,6 +373,7 @@ export function main() {
animations: [],
template: null,
templateUrl: null,
htmlAst: null,
ngContentSelectors: [],
externalStylesheets: [],
styleUrls: [],

View File

@ -34,12 +34,13 @@ export class CompilerImpl implements Compiler {
private _delegate: JitCompiler;
constructor(
private _injector: Injector, private _metadataResolver: CompileMetadataResolver,
templateParser: TemplateParser, styleCompiler: StyleCompiler, viewCompiler: ViewCompiler,
ngModuleCompiler: NgModuleCompiler, summaryResolver: SummaryResolver<Type<any>>,
compileReflector: CompileReflector, compilerConfig: CompilerConfig, console: Console) {
htmlParser: HtmlParser, templateParser: TemplateParser, styleCompiler: StyleCompiler,
viewCompiler: ViewCompiler, ngModuleCompiler: NgModuleCompiler,
summaryResolver: SummaryResolver<Type<any>>, compileReflector: CompileReflector,
compilerConfig: CompilerConfig, console: Console) {
this._delegate = new JitCompiler(
_metadataResolver, templateParser, styleCompiler, viewCompiler, ngModuleCompiler,
summaryResolver, compileReflector, compilerConfig, console,
_metadataResolver, htmlParser, templateParser, styleCompiler, viewCompiler,
ngModuleCompiler, summaryResolver, compileReflector, compilerConfig, console,
this.getExtraNgModuleProviders.bind(this));
}
@ -141,7 +142,7 @@ export const COMPILER_PROVIDERS = <StaticProvider[]>[
{ provide: NgModuleCompiler, deps: [CompileReflector] },
{ provide: CompilerConfig, useValue: new CompilerConfig()},
{ provide: Compiler, useClass: CompilerImpl, deps: [Injector, CompileMetadataResolver,
TemplateParser, StyleCompiler,
HtmlParser, TemplateParser, StyleCompiler,
ViewCompiler, NgModuleCompiler,
SummaryResolver, CompileReflector, CompilerConfig,
Console]},