fix(ivy): verify Host Bindings and Host Listeners before compiling them (#28356)

Prior to this change we may encounter some errors (like pipes being used where they should not be used) while compiling Host Bindings and Listeners. With this update we move validation logic to the analyze phase and throw an error if something is wrong. This also aligns error messages between Ivy and VE.

PR Close #28356
This commit is contained in:
Andrew Kushnir
2019-01-24 17:25:46 -08:00
committed by Jason Aden
parent ad499628cb
commit 76cedb8bf3
11 changed files with 175 additions and 49 deletions

View File

@ -98,7 +98,7 @@ export {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata
export {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler';
export {makeBindingParser, parseTemplate} from './render3/view/template';
export {R3Reference} from './render3/util';
export {compileBaseDefFromMetadata, R3BaseRefMetaData, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings} from './render3/view/compiler';
export {compileBaseDefFromMetadata, R3BaseRefMetaData, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, verifyHostBindings} from './render3/view/compiler';
export {publishFacade} from './jit_compiler_facade';
// This file only reexports content of the `src` folder. Keep it that way.

View File

@ -38,6 +38,8 @@ export interface CompilerFacade {
compileComponent(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3ComponentMetadataFacade): any;
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan;
R3ResolvedDependencyType: typeof R3ResolvedDependencyType;
}
@ -109,7 +111,7 @@ export interface R3DirectiveMetadataFacade {
name: string;
type: any;
typeArgumentCount: number;
typeSourceSpan: null;
typeSourceSpan: ParseSourceSpan;
deps: R3DependencyMetadataFacade[]|null;
selector: string|null;
queries: R3QueryMetadataFacade[];
@ -148,3 +150,9 @@ export interface R3QueryMetadataFacade {
descendants: boolean;
read: any|null;
}
export interface ParseSourceSpan {
start: any;
end: any;
details: any;
}

View File

@ -13,13 +13,14 @@ import {HostBinding, HostListener, Input, Output, Type} from './core';
import {compileInjectable} from './injectable_compiler_2';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/interpolation_config';
import {Expression, LiteralExpr, WrappedNodeExpr} from './output/output_ast';
import {ParseError, ParseSourceSpan, r3JitTypeSourceSpan} from './parse_util';
import {R3DependencyMetadata, R3ResolvedDependencyType} from './render3/r3_factory';
import {jitExpression} from './render3/r3_jit';
import {R3InjectorMetadata, R3NgModuleMetadata, compileInjector, compileNgModule} from './render3/r3_module_compiler';
import {compilePipeFromMetadata} from './render3/r3_pipe_compiler';
import {R3Reference} from './render3/util';
import {R3DirectiveMetadata, R3QueryMetadata} from './render3/view/api';
import {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings} from './render3/view/compiler';
import {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, verifyHostBindings} from './render3/view/compiler';
import {makeBindingParser, parseTemplate} from './render3/view/template';
import {DomElementSchemaRegistry} from './schema/dom_element_schema_registry';
@ -142,6 +143,10 @@ export class CompilerFacadeImpl implements CompilerFacade {
return jitExpression(res.expression, angularCoreEnv, sourceMapUrl, preStatements);
}
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan {
return r3JitTypeSourceSpan(kind, typeName, sourceUrl);
}
}
// This seems to be needed to placate TS v3.0 only
@ -189,10 +194,10 @@ function convertDirectiveFacadeToMetadata(facade: R3DirectiveMetadataFacade): R3
return {
...facade as R3DirectiveMetadataFacadeNoPropAndWhitespace,
typeSourceSpan: null !,
typeSourceSpan: facade.typeSourceSpan,
type: new WrappedNodeExpr(facade.type),
deps: convertR3DependencyMetadataArray(facade.deps),
host: extractHostBindings(facade.host, facade.propMetadata),
host: extractHostBindings(facade.host, facade.propMetadata, facade.typeSourceSpan),
inputs: {...inputsFromMetadata, ...inputsFromType},
outputs: {...outputsFromMetadata, ...outputsFromType},
queries: facade.queries.map(convertToR3QueryMetadata),
@ -244,28 +249,36 @@ function convertR3DependencyMetadataArray(facades: R3DependencyMetadataFacade[]
return facades == null ? null : facades.map(convertR3DependencyMetadata);
}
function extractHostBindings(host: {[key: string]: string}, propMetadata: {[key: string]: any[]}): {
function extractHostBindings(
host: {[key: string]: string}, propMetadata: {[key: string]: any[]},
sourceSpan: ParseSourceSpan): {
attributes: StringMap,
listeners: StringMap,
properties: StringMap,
} {
// First parse the declarations from the metadata.
const {attributes, listeners, properties} = parseHostBindings(host || {});
const bindings = parseHostBindings(host || {});
// After that check host bindings for errors
const errors = verifyHostBindings(bindings, sourceSpan);
if (errors.length) {
throw new Error(errors.map((error: ParseError) => error.msg).join('\n'));
}
// Next, loop over the properties of the object, looking for @HostBinding and @HostListener.
for (const field in propMetadata) {
if (propMetadata.hasOwnProperty(field)) {
propMetadata[field].forEach(ann => {
if (isHostBinding(ann)) {
properties[ann.hostPropertyName || field] = field;
bindings.properties[ann.hostPropertyName || field] = field;
} else if (isHostListener(ann)) {
listeners[ann.eventName || field] = `${field}(${(ann.args || []).join(',')})`;
bindings.listeners[ann.eventName || field] = `${field}(${(ann.args || []).join(',')})`;
}
});
}
}
return {attributes, listeners, properties};
return bindings;
}
function isHostBinding(value: any): value is HostBinding {

View File

@ -139,3 +139,19 @@ export function typeSourceSpan(kind: string, type: CompileIdentifierMetadata): P
return new ParseSourceSpan(
new ParseLocation(sourceFile, -1, -1, -1), new ParseLocation(sourceFile, -1, -1, -1));
}
/**
* Generates Source Span object for a given R3 Type for JIT mode.
*
* @param kind Component or Directive.
* @param typeName name of the Component or Directive.
* @param sourceUrl reference to Component or Directive source.
* @returns instance of ParseSourceSpan that represent a given Component or Directive.
*/
export function r3JitTypeSourceSpan(
kind: string, typeName: string, sourceUrl: string): ParseSourceSpan {
const sourceFileName = `in ${kind} ${typeName} in ${sourceUrl}`;
const sourceFile = new ParseSourceFile('', sourceFileName);
return new ParseSourceSpan(
new ParseLocation(sourceFile, -1, -1, -1), new ParseLocation(sourceFile, -1, -1, -1));
}

View File

@ -16,7 +16,7 @@ import {AST, ParsedEvent, ParsedEventType, ParsedProperty} from '../../expressio
import {LifecycleHooks} from '../../lifecycle_reflector';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
import * as o from '../../output/output_ast';
import {typeSourceSpan} from '../../parse_util';
import {ParseError, ParseSourceSpan, typeSourceSpan} from '../../parse_util';
import {CssSelector, SelectorMatcher} from '../../selector';
import {ShadowCss} from '../../shadow_css';
import {CONTENT_ATTR, HOST_ATTR} from '../../style_compiler';
@ -30,7 +30,7 @@ import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, type
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
import {Instruction, StylingBuilder} from './styling_builder';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template';
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
const EMPTY_ARRAY: any[] = [];
@ -865,11 +865,15 @@ const enum HostBindingGroup {
Event = 2,
}
export function parseHostBindings(host: {[key: string]: string}): {
attributes: {[key: string]: string},
listeners: {[key: string]: string},
properties: {[key: string]: string},
} {
// Defines Host Bindings structure that contains attributes, listeners, and properties,
// parsed from the `host` object defined for a Type.
export interface ParsedHostBindings {
attributes: {[key: string]: string};
listeners: {[key: string]: string};
properties: {[key: string]: string};
}
export function parseHostBindings(host: {[key: string]: string}): ParsedHostBindings {
const attributes: {[key: string]: string} = {};
const listeners: {[key: string]: string} = {};
const properties: {[key: string]: string} = {};
@ -892,6 +896,25 @@ export function parseHostBindings(host: {[key: string]: string}): {
return {attributes, listeners, properties};
}
/**
* Verifies host bindings and returns the list of errors (if any). Empty array indicates that a
* given set of host bindings has no errors.
*
* @param bindings set of host bindings to verify.
* @param sourceSpan source span where host bindings were defined.
* @returns array of errors associated with a given set of host bindings.
*/
export function verifyHostBindings(
bindings: ParsedHostBindings, sourceSpan: ParseSourceSpan): ParseError[] {
const summary = metadataAsSummary({ host: bindings } as any);
// TODO: abstract out host bindings verification logic and use it instead of
// creating events and properties ASTs to detect errors (FW-996)
const bindingParser = makeBindingParser();
bindingParser.createDirectiveHostEventAsts(summary, sourceSpan);
bindingParser.createBoundHostProperties(summary, sourceSpan);
return bindingParser.errors;
}
function compileStyles(styles: string[], selector: string, hostSelector: string): string[] {
const shadowCss = new ShadowCss();
return styles.map(style => { return shadowCss !.shimCssText(style, selector, hostSelector); });