From 996c7c2dde3f2f8a2beb3a482fe6881f8532f299 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Mon, 11 Sep 2017 15:18:19 -0700 Subject: [PATCH] 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 `
`). - 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 --- .../src/diagnostics/check_types.ts | 231 -------------- .../src/diagnostics/translate_diagnostics.ts | 57 ++++ .../compiler-cli/src/language_services.ts | 1 - packages/compiler-cli/src/perform_compile.ts | 6 +- packages/compiler-cli/src/transformers/api.ts | 12 + .../compiler-cli/src/transformers/program.ts | 66 ++-- .../compiler-cli/src/transformers/util.ts | 9 + .../test/diagnostics/check_types_spec.ts | 293 ++++++++++++------ packages/compiler/src/aot/compiler.ts | 69 ++++- packages/compiler/src/aot/compiler_factory.ts | 7 +- packages/compiler/src/aot/compiler_options.ts | 1 + packages/compiler/src/compile_metadata.ts | 20 +- packages/compiler/src/config.ts | 3 +- packages/compiler/src/directive_normalizer.ts | 4 +- packages/compiler/src/jit/compiler.ts | 13 +- packages/compiler/src/metadata_resolver.ts | 1 + packages/compiler/src/output/output_ast.ts | 4 +- .../src/template_parser/template_parser.ts | 17 +- .../src/view_compiler/type_check_compiler.ts | 283 +++++++++++++++++ .../test/directive_normalizer_spec.ts | 1 + .../template_parser/template_parser_spec.ts | 2 + .../src/compiler_factory.ts | 13 +- 22 files changed, 712 insertions(+), 401 deletions(-) delete mode 100644 packages/compiler-cli/src/diagnostics/check_types.ts create mode 100644 packages/compiler-cli/src/diagnostics/translate_diagnostics.ts create mode 100644 packages/compiler-cli/src/transformers/util.ts create mode 100644 packages/compiler/src/view_compiler/type_check_compiler.ts diff --git a/packages/compiler-cli/src/diagnostics/check_types.ts b/packages/compiler-cli/src/diagnostics/check_types.ts deleted file mode 100644 index a3d64fcbd9..0000000000 --- a/packages/compiler-cli/src/diagnostics/check_types.ts +++ /dev/null @@ -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; - -const stubCancellationToken: ts.CancellationToken = { - isCancellationRequested(): boolean{return false;}, - throwIfCancellationRequested(): void{} -}; - -export class TypeChecker { - private _aotCompiler: AotCompiler|undefined; - private _reflector: StaticReflector|undefined; - private _factories: Map|undefined; - private _factoryNames: string[]|undefined; - private _diagnosticProgram: ts.Program|undefined; - private _diagnosticsByFile: Map|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 { - 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 { - 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(); - 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) {} - - 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); - } -} diff --git a/packages/compiler-cli/src/diagnostics/translate_diagnostics.ts b/packages/compiler-cli/src/diagnostics/translate_diagnostics.ts new file mode 100644 index 0000000000..b15543cd0a --- /dev/null +++ b/packages/compiler-cli/src/diagnostics/translate_diagnostics.ts @@ -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'); +} diff --git a/packages/compiler-cli/src/language_services.ts b/packages/compiler-cli/src/language_services.ts index a875c96a7f..b5b27364d5 100644 --- a/packages/compiler-cli/src/language_services.ts +++ b/packages/compiler-cli/src/language_services.ts @@ -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'; diff --git a/packages/compiler-cli/src/perform_compile.ts b/packages/compiler-cli/src/perform_compile.ts index 7408a0b29b..67908cbc7f 100644 --- a/packages/compiler-cli/src/perform_compile.ts +++ b/packages/compiler-cli/src/perform_compile.ts @@ -18,10 +18,6 @@ const TS_EXT = /\.ts$/; export type Diagnostics = Array; -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]; diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 04f0e9dbdc..1f340efab7 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -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; diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 17035dff26..017afabe29 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -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 { @@ -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[] = []; 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 { @@ -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; + 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); diff --git a/packages/compiler-cli/src/transformers/util.ts b/packages/compiler-cli/src/transformers/util.ts new file mode 100644 index 0000000000..7e14366c6e --- /dev/null +++ b/packages/compiler-cli/src/transformers/util.ts @@ -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)$/; diff --git a/packages/compiler-cli/test/diagnostics/check_types_spec.ts b/packages/compiler-cli/test/diagnostics/check_types_spec.ts index 26dfc85dbe..a7d505a6f3 100644 --- a/packages/compiler-cli/test/diagnostics/check_types_spec.ts +++ b/packages/compiler-cli/test/diagnostics/check_types_spec.ts @@ -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('
{{fame}}
', `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('
{{maybePerson.name}}
', `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('
{{getFame()}}
', + `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('
{{getPerson().fame}}
', `Property 'fame' does not exist on type 'Person'.`, + '0:5'); + }); + it('should reject an access to a nullable field of a method result', () => { + r('
{{getMaybePerson().name}}
', `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( + '{{fame}}', + `Property 'fame' does not exist on type 'AppComponent'.`, '0:13'); + }); + it('should report an invalid call to a pipe', () => { + rejectOnlyWithFullTemplateTypeCheck( + '
{{"hello" | aPipe}}
', + `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( + '
{{aDir.fname}}
', + `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,63 +224,69 @@ 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('

Hello {{name}}

'), - 'app.module.ts': ` - import { NgModule } from '@angular/core'; - import { toString } from './utils'; +const QUICKSTART = { + 'src/app.component.ts': appComponentSource(), + 'src/app.component.html': '

Hello {{name}}

', + 'src/app.module.ts': ` + import { NgModule } from '@angular/core'; + import { AppComponent, APipe, ADirective } from './app.component'; - import { AppComponent } from './app.component'; - - @NgModule({ - declarations: [ AppComponent ], - bootstrap: [ AppComponent ] - }) - export class AppModule { } - ` - } - } + @NgModule({ + declarations: [ AppComponent, APipe, ADirective ], + bootstrap: [ AppComponent ] + }) + export class AppModule { } + ` }; -const LOWERING_QUICKSTART: MockDirectory = { - quickstart: { - app: { - 'app.component.ts': appComponentSource('

Hello {{name}}

'), - 'app.module.ts': ` - import { NgModule, Component } from '@angular/core'; - import { toString } from './utils'; +const LOWERING_QUICKSTART = { + 'src/app.component.ts': appComponentSource(), + 'src/app.component.html': '

Hello {{name}}

', + 'src/app.module.ts': ` + import { NgModule, Component } from '@angular/core'; - import { AppComponent } from './app.component'; + import { AppComponent, APipe, ADirective } from './app.component'; - class Foo {} + class Foo {} - @Component({ - template: '', - providers: [ - {provide: 'someToken', useFactory: () => new Foo()} - ] - }) - export class Bar {} + @Component({ + template: '', + providers: [ + {provide: 'someToken', useFactory: () => new Foo()} + ] + }) + export class Bar {} - @NgModule({ - declarations: [ AppComponent, Bar ], - bootstrap: [ AppComponent ] - }) - export class AppModule { } - ` - } - } + @NgModule({ + 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)); } } diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index 98ec4b3cdb..aecae17915 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -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(); + 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, 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, + 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(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)) { diff --git a/packages/compiler/src/aot/compiler_factory.ts b/packages/compiler/src/aot/compiler_factory.ts index 6fea0d80a7..4e96744587 100644 --- a/packages/compiler/src/aot/compiler_factory.ts +++ b/packages/compiler/src/aot/compiler_factory.ts @@ -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}; diff --git a/packages/compiler/src/aot/compiler_options.ts b/packages/compiler/src/aot/compiler_options.ts index 5528e9b5d2..662ee488dc 100644 --- a/packages/compiler/src/aot/compiler_options.ts +++ b/packages/compiler/src/aot/compiler_options.ts @@ -16,4 +16,5 @@ export interface AotCompilerOptions { enableLegacyTemplate?: boolean; enableSummariesForJit?: boolean; preserveWhitespaces?: boolean; + fullTemplateTypeCheck?: boolean; } diff --git a/packages/compiler/src/compile_metadata.ts b/packages/compiler/src/compile_metadata.ts index 82829da690..32313b0fc7 100644 --- a/packages/compiler/src/compile_metadata.ts +++ b/packages/compiler/src/compile_metadata.ts @@ -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: [], diff --git a/packages/compiler/src/config.ts b/packages/compiler/src/config.ts index 935b13c480..3be0286523 100644 --- a/packages/compiler/src/config.ts +++ b/packages/compiler/src/config.ts @@ -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; diff --git a/packages/compiler/src/directive_normalizer.ts b/packages/compiler/src/directive_normalizer.ts index 2ac66f6f76..445b4f4d52 100644 --- a/packages/compiler/src/directive_normalizer.ts +++ b/packages/compiler/src/directive_normalizer.ts @@ -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, diff --git a/packages/compiler/src/jit/compiler.ts b/packages/compiler/src/jit/compiler.ts index dfc508a4aa..5b913712dd 100644 --- a/packages/compiler/src/jit/compiler.ts +++ b/packages/compiler/src/jit/compiler.ts @@ -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, - 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, 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); diff --git a/packages/compiler/src/metadata_resolver.ts b/packages/compiler/src/metadata_resolver.ts index 3392966529..334a20332d 100644 --- a/packages/compiler/src/metadata_resolver.ts +++ b/packages/compiler/src/metadata_resolver.ts @@ -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 || [], diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index 4f36042f0d..712f046d8b 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -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( diff --git a/packages/compiler/src/template_parser/template_parser.ts b/packages/compiler/src/template_parser/template_parser.ts index 17f14a7d4c..47a56f1b3a 100644 --- a/packages/compiler/src/template_parser/template_parser.ts +++ b/packages/compiler/src/template_parser/template_parser.ts @@ -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); diff --git a/packages/compiler/src/view_compiler/type_check_compiler.ts b/packages/compiler/src/view_compiler/type_check_compiler.ts new file mode 100644 index 0000000000..dc441c38c5 --- /dev/null +++ b/packages/compiler/src/view_compiler/type_check_compiler.ts @@ -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(); + 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(); + private outputVarNames = new Map(); + private refOutputVars = new Map(); + 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, 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 = ast.value; + const inter = 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 {} +} diff --git a/packages/compiler/test/directive_normalizer_spec.ts b/packages/compiler/test/directive_normalizer_spec.ts index 0ed2fa5b77..6c16c9ca3d 100644 --- a/packages/compiler/test/directive_normalizer_spec.ts +++ b/packages/compiler/test/directive_normalizer_spec.ts @@ -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 || [], diff --git a/packages/compiler/test/template_parser/template_parser_spec.ts b/packages/compiler/test/template_parser/template_parser_spec.ts index b2db790cf7..1e22382b4a 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_spec.ts @@ -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: [], diff --git a/packages/platform-browser-dynamic/src/compiler_factory.ts b/packages/platform-browser-dynamic/src/compiler_factory.ts index b146f84402..c2a2fb3cc0 100644 --- a/packages/platform-browser-dynamic/src/compiler_factory.ts +++ b/packages/platform-browser-dynamic/src/compiler_factory.ts @@ -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>, - compileReflector: CompileReflector, compilerConfig: CompilerConfig, console: Console) { + htmlParser: HtmlParser, templateParser: TemplateParser, styleCompiler: StyleCompiler, + viewCompiler: ViewCompiler, ngModuleCompiler: NgModuleCompiler, + summaryResolver: SummaryResolver>, 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 = [ { 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]},