refactor(ivy): move classMap interpolation logic internally (#31211)

Adds the new `classMapInterpolate1` through `classMapInterpolate8` instructions which handle interpolations inside the `class` attribute and moves the interpolation logic internally. This allows us to remove the `interpolationX` instructions in a follow-up PR.

These changes also add an error if an interpolation is encountered inside a `style` tag (e.g. `style="width: {{value}}"`). Up until now this would actually generate valid instructions, because `styleMap` goes through the same code path as `classMap` which does support interpolation. At runtime, however, `styleMap` would set invalid styles that look like `<div style="0:w;1:i;2:d;3:t;4:h;5::;7:1;">`. In `ViewEngine` interpolations inside `style` weren't supported either, however there we'd output invalid styles like `<div style="unsafe">`, even if the content was trusted.

PR Close #31211
This commit is contained in:
crisbeto
2019-06-27 18:57:09 +02:00
committed by Alex Rickabaugh
parent dca713c087
commit 02491a6ce8
15 changed files with 1469 additions and 89 deletions

View File

@ -72,8 +72,46 @@ export class Identifiers {
static classMap: o.ExternalReference = {name: 'ɵɵclassMap', moduleName: CORE};
static classMapInterpolate1:
o.ExternalReference = {name: 'ɵɵclassMapInterpolate1', moduleName: CORE};
static classMapInterpolate2:
o.ExternalReference = {name: 'ɵɵclassMapInterpolate2', moduleName: CORE};
static classMapInterpolate3:
o.ExternalReference = {name: 'ɵɵclassMapInterpolate3', moduleName: CORE};
static classMapInterpolate4:
o.ExternalReference = {name: 'ɵɵclassMapInterpolate4', moduleName: CORE};
static classMapInterpolate5:
o.ExternalReference = {name: 'ɵɵclassMapInterpolate5', moduleName: CORE};
static classMapInterpolate6:
o.ExternalReference = {name: 'ɵɵclassMapInterpolate6', moduleName: CORE};
static classMapInterpolate7:
o.ExternalReference = {name: 'ɵɵclassMapInterpolate7', moduleName: CORE};
static classMapInterpolate8:
o.ExternalReference = {name: 'ɵɵclassMapInterpolate8', moduleName: CORE};
static classMapInterpolateV:
o.ExternalReference = {name: 'ɵɵclassMapInterpolateV', moduleName: CORE};
static styleProp: o.ExternalReference = {name: 'ɵɵstyleProp', moduleName: CORE};
static stylePropInterpolate1:
o.ExternalReference = {name: 'ɵɵstylePropInterpolate1', moduleName: CORE};
static stylePropInterpolate2:
o.ExternalReference = {name: 'ɵɵstylePropInterpolate2', moduleName: CORE};
static stylePropInterpolate3:
o.ExternalReference = {name: 'ɵɵstylePropInterpolate3', moduleName: CORE};
static stylePropInterpolate4:
o.ExternalReference = {name: 'ɵɵstylePropInterpolate4', moduleName: CORE};
static stylePropInterpolate5:
o.ExternalReference = {name: 'ɵɵstylePropInterpolate5', moduleName: CORE};
static stylePropInterpolate6:
o.ExternalReference = {name: 'ɵɵstylePropInterpolate6', moduleName: CORE};
static stylePropInterpolate7:
o.ExternalReference = {name: 'ɵɵstylePropInterpolate7', moduleName: CORE};
static stylePropInterpolate8:
o.ExternalReference = {name: 'ɵɵstylePropInterpolate8', moduleName: CORE};
static stylePropInterpolateV:
o.ExternalReference = {name: 'ɵɵstylePropInterpolateV', moduleName: CORE};
static stylingApply: o.ExternalReference = {name: 'ɵɵstylingApply', moduleName: CORE};
static styleSanitizer: o.ExternalReference = {name: 'ɵɵstyleSanitizer', moduleName: CORE};

View File

@ -12,7 +12,7 @@ import {CompileReflector} from '../../compile_reflector';
import {BindingForm, convertPropertyBinding} from '../../compiler_util/expression_converter';
import {ConstantPool, DefinitionKind} from '../../constant_pool';
import * as core from '../../core';
import {AST, ParsedEvent, ParsedEventType, ParsedProperty} from '../../expression_parser/ast';
import {AST, Interpolation, ParsedEvent, ParsedEventType, ParsedProperty} from '../../expression_parser/ast';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
import * as o from '../../output/output_ast';
import {ParseError, ParseSourceSpan, typeSourceSpan} from '../../parse_util';
@ -28,7 +28,7 @@ import {Render3ParseResult} from '../r3_template_transform';
import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from './api';
import {Instruction, StylingBuilder} from './styling_builder';
import {StylingBuilder, StylingInstruction} from './styling_builder';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template';
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, chainedInstruction, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
@ -777,8 +777,8 @@ function bindingFn(implicit: any, value: AST) {
}
function createStylingStmt(
instruction: Instruction, bindingContext: any, bindingFn: Function): o.Statement {
const params = instruction.buildParams(value => bindingFn(bindingContext, value).currValExpr);
instruction: StylingInstruction, bindingContext: any, bindingFn: Function): o.Statement {
const params = instruction.params(value => bindingFn(bindingContext, value).currValExpr);
return o.importExpr(instruction.reference, null, instruction.sourceSpan)
.callFn(params, instruction.sourceSpan)
.toStmt();

View File

@ -11,23 +11,26 @@ import {AST, BindingType, Interpolation} from '../../expression_parser/ast';
import * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util';
import {isEmptyExpression} from '../../template_parser/template_parser';
import {error} from '../../util';
import * as t from '../r3_ast';
import {Identifiers as R3} from '../r3_identifiers';
import {parse as parseStyle} from './style_parser';
import {compilerIsNewStylingInUse} from './styling_state';
import {ValueConverter} from './template';
import {getInterpolationArgsLength} from './util';
const IMPORTANT_FLAG = '!important';
/**
* A styling expression summary that is to be processed by the compiler
*/
export interface Instruction {
export interface StylingInstruction {
sourceSpan: ParseSourceSpan|null;
reference: o.ExternalReference;
allocateBindingSlots: number;
buildParams(convertFn: (value: any) => o.Expression): o.Expression[];
supportsInterpolation?: boolean;
params: ((convertFn: (value: any) => o.Expression | o.Expression[]) => o.Expression[]);
}
/**
@ -261,13 +264,13 @@ export class StylingBuilder {
*/
buildHostAttrsInstruction(
sourceSpan: ParseSourceSpan|null, attrs: o.Expression[],
constantPool: ConstantPool): Instruction|null {
constantPool: ConstantPool): StylingInstruction|null {
if (this._directiveExpr && (attrs.length || this._hasInitialValues)) {
return {
sourceSpan,
reference: R3.elementHostAttrs,
allocateBindingSlots: 0,
buildParams: () => {
params: () => {
// params => elementHostAttrs(attrs)
this.populateInitialStylingAttrs(attrs);
const attrArray = !attrs.some(attr => attr instanceof o.WrappedNodeExpr) ?
@ -286,14 +289,14 @@ export class StylingBuilder {
* The instruction generation code below is used for producing the AOT statement code which is
* responsible for registering style/class bindings to an element.
*/
buildStylingInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool): Instruction
|null {
buildStylingInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
StylingInstruction|null {
if (this.hasBindings) {
return {
sourceSpan,
allocateBindingSlots: 0,
reference: R3.styling,
buildParams: () => {
params: () => {
// a string array of every style-based binding
const styleBindingProps =
this._singleStyleInputs ? this._singleStyleInputs.map(i => o.literal(i.name)) : [];
@ -344,7 +347,7 @@ export class StylingBuilder {
* The instruction data will contain all expressions for `classMap` to function
* which includes the `[class]` expression params.
*/
buildClassMapInstruction(valueConverter: ValueConverter): Instruction|null {
buildClassMapInstruction(valueConverter: ValueConverter): StylingInstruction|null {
if (this._classMapInput) {
return this._buildMapBasedInstruction(valueConverter, true, this._classMapInput);
}
@ -357,7 +360,7 @@ export class StylingBuilder {
* The instruction data will contain all expressions for `styleMap` to function
* which includes the `[style]` expression params.
*/
buildStyleMapInstruction(valueConverter: ValueConverter): Instruction|null {
buildStyleMapInstruction(valueConverter: ValueConverter): StylingInstruction|null {
if (this._styleMapInput) {
return this._buildMapBasedInstruction(valueConverter, false, this._styleMapInput);
}
@ -365,7 +368,8 @@ export class StylingBuilder {
}
private _buildMapBasedInstruction(
valueConverter: ValueConverter, isClassBased: boolean, stylingInput: BoundStylingEntry) {
valueConverter: ValueConverter, isClassBased: boolean,
stylingInput: BoundStylingEntry): StylingInstruction {
let totalBindingSlotsRequired = 0;
if (compilerIsNewStylingInUse()) {
// the old implementation does not reserve slot values for
@ -377,27 +381,42 @@ export class StylingBuilder {
// be evaluated (the AST visit call) during creation time so that any
// pipes can be picked up in time before the template is built
const mapValue = stylingInput.value.visit(valueConverter);
if (mapValue instanceof Interpolation) {
let reference: o.ExternalReference;
if (mapValue instanceof Interpolation && isClassBased) {
totalBindingSlotsRequired += mapValue.expressions.length;
reference = getClassMapInterpolationExpression(mapValue);
} else {
reference = isClassBased ? R3.classMap : R3.styleMap;
}
const reference = isClassBased ? R3.classMap : R3.styleMap;
return {
sourceSpan: stylingInput.sourceSpan,
reference,
allocateBindingSlots: totalBindingSlotsRequired,
buildParams: (convertFn: (value: any) => o.Expression) => { return [convertFn(mapValue)]; }
supportsInterpolation: isClassBased,
params: (convertFn: (value: any) => o.Expression | o.Expression[]) => {
const convertResult = convertFn(mapValue);
return Array.isArray(convertResult) ? convertResult : [convertResult];
}
};
}
private _buildSingleInputs(
reference: o.ExternalReference, inputs: BoundStylingEntry[], mapIndex: Map<string, number>,
allowUnits: boolean, valueConverter: ValueConverter): Instruction[] {
allowUnits: boolean, valueConverter: ValueConverter,
getInterpolationExpressionFn?: (value: Interpolation) => o.ExternalReference):
StylingInstruction[] {
let totalBindingSlotsRequired = 0;
return inputs.map(input => {
const bindingIndex: number = mapIndex.get(input.name !) !;
const value = input.value.visit(valueConverter);
totalBindingSlotsRequired += (value instanceof Interpolation) ? value.expressions.length : 0;
if (value instanceof Interpolation) {
totalBindingSlotsRequired += value.expressions.length;
if (getInterpolationExpressionFn) {
reference = getInterpolationExpressionFn(value);
}
}
if (compilerIsNewStylingInUse()) {
// the old implementation does not reserve slot values for
// binding entries. The new one does.
@ -405,14 +424,18 @@ export class StylingBuilder {
}
return {
sourceSpan: input.sourceSpan,
supportsInterpolation: !!getInterpolationExpressionFn,
allocateBindingSlots: totalBindingSlotsRequired, reference,
buildParams: (convertFn: (value: any) => o.Expression) => {
params: (convertFn: (value: any) => o.Expression | o.Expression[]) => {
// min params => stylingProp(elmIndex, bindingIndex, value)
// max params => stylingProp(elmIndex, bindingIndex, value, overrideFlag)
const params: o.Expression[] = [];
params.push(o.literal(bindingIndex));
params.push(convertFn(value));
const params: o.Expression[] = [o.literal(bindingIndex)];
const convertResult = convertFn(value);
if (Array.isArray(convertResult)) {
params.push(...convertResult);
} else {
params.push(convertResult);
}
if (allowUnits) {
if (input.unit) {
params.push(o.literal(input.unit));
@ -431,7 +454,7 @@ export class StylingBuilder {
});
}
private _buildClassInputs(valueConverter: ValueConverter): Instruction[] {
private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] {
if (this._singleClassInputs) {
return this._buildSingleInputs(
R3.classProp, this._singleClassInputs, this._classesIndex, false, valueConverter);
@ -439,29 +462,30 @@ export class StylingBuilder {
return [];
}
private _buildStyleInputs(valueConverter: ValueConverter): Instruction[] {
private _buildStyleInputs(valueConverter: ValueConverter): StylingInstruction[] {
if (this._singleStyleInputs) {
return this._buildSingleInputs(
R3.styleProp, this._singleStyleInputs, this._stylesIndex, true, valueConverter);
R3.styleProp, this._singleStyleInputs, this._stylesIndex, true, valueConverter,
getStylePropInterpolationExpression);
}
return [];
}
private _buildApplyFn(): Instruction {
private _buildApplyFn(): StylingInstruction {
return {
sourceSpan: this._lastStylingInput ? this._lastStylingInput.sourceSpan : null,
reference: R3.stylingApply,
allocateBindingSlots: 0,
buildParams: () => { return []; }
params: () => { return []; }
};
}
private _buildSanitizerFn() {
private _buildSanitizerFn(): StylingInstruction {
return {
sourceSpan: this._firstStylingInput ? this._firstStylingInput.sourceSpan : null,
reference: R3.styleSanitizer,
allocateBindingSlots: 0,
buildParams: () => [o.importExpr(R3.defaultStyleSanitizer)]
params: () => [o.importExpr(R3.defaultStyleSanitizer)]
};
}
@ -470,7 +494,7 @@ export class StylingBuilder {
* into the update block of a template function or a directive hostBindings function.
*/
buildUpdateLevelInstructions(valueConverter: ValueConverter) {
const instructions: Instruction[] = [];
const instructions: StylingInstruction[] = [];
if (this.hasBindings) {
if (compilerIsNewStylingInUse() && this._useDefaultSanitizer) {
instructions.push(this._buildSanitizerFn());
@ -548,3 +572,61 @@ export function parseProperty(name: string):
return {property, unit, hasOverrideFlag};
}
/**
* Gets the instruction to generate for an interpolated class map.
* @param interpolation An Interpolation AST
*/
function getClassMapInterpolationExpression(interpolation: Interpolation): o.ExternalReference {
switch (getInterpolationArgsLength(interpolation)) {
case 1:
return R3.classMap;
case 3:
return R3.classMapInterpolate1;
case 5:
return R3.classMapInterpolate2;
case 7:
return R3.classMapInterpolate3;
case 9:
return R3.classMapInterpolate4;
case 11:
return R3.classMapInterpolate5;
case 13:
return R3.classMapInterpolate6;
case 15:
return R3.classMapInterpolate7;
case 17:
return R3.classMapInterpolate8;
default:
return R3.classMapInterpolateV;
}
}
/**
* Gets the instruction to generate for an interpolated style prop.
* @param interpolation An Interpolation AST
*/
function getStylePropInterpolationExpression(interpolation: Interpolation) {
switch (getInterpolationArgsLength(interpolation)) {
case 1:
return R3.styleProp;
case 3:
return R3.stylePropInterpolate1;
case 5:
return R3.stylePropInterpolate2;
case 7:
return R3.stylePropInterpolate3;
case 9:
return R3.stylePropInterpolate4;
case 11:
return R3.stylePropInterpolate5;
case 13:
return R3.stylePropInterpolate6;
case 15:
return R3.stylePropInterpolate7;
case 17:
return R3.stylePropInterpolate8;
default:
return R3.stylePropInterpolateV;
}
}

View File

@ -36,8 +36,8 @@ import {I18nContext} from './i18n/context';
import {I18nMetaVisitor} from './i18n/meta';
import {getSerializedI18nContent} from './i18n/serializer';
import {I18N_ICU_MAPPING_PREFIX, TRANSLATION_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
import {Instruction, StylingBuilder} from './styling_builder';
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, chainedInstruction, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
import {StylingBuilder, StylingInstruction} from './styling_builder';
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, chainedInstruction, getAttrsForDirectiveMatching, getInterpolationArgsLength, invalid, trimTrailingNulls, unsupported} from './util';
@ -1067,14 +1067,21 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}
private processStylingInstruction(
elementIndex: number, instruction: Instruction|null, createMode: boolean) {
elementIndex: number, instruction: StylingInstruction|null, createMode: boolean) {
if (instruction) {
const paramsFn = () => instruction.buildParams(value => this.convertPropertyBinding(value));
if (createMode) {
this.creationInstruction(instruction.sourceSpan, instruction.reference, paramsFn);
this.creationInstruction(instruction.sourceSpan, instruction.reference, () => {
return instruction.params(value => this.convertPropertyBinding(value)) as o.Expression[];
});
} else {
this.updateInstruction(
elementIndex, instruction.sourceSpan, instruction.reference, paramsFn);
this.updateInstruction(elementIndex, instruction.sourceSpan, instruction.reference, () => {
return instruction
.params(value => {
return (instruction.supportsInterpolation && value instanceof Interpolation) ?
this.getUpdateInstructionArguments(value) :
this.convertPropertyBinding(value);
}) as o.Expression[];
});
}
}
}
@ -1150,12 +1157,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}
private convertPropertyBinding(value: AST): o.Expression {
const interpolationFn =
value instanceof Interpolation ? interpolate : () => error('Unexpected interpolation');
const convertedPropertyBinding = convertPropertyBinding(
this, this.getImplicitReceiverExpr(), value, this.bindingContext(), BindingForm.TrySimple,
interpolationFn);
() => error('Unexpected interpolation'));
const valExpr = convertedPropertyBinding.currValExpr;
this._tempVariables.push(...convertedPropertyBinding.stmts);
@ -1751,31 +1755,6 @@ function getNgProjectAsLiteral(attribute: t.TextAttribute): o.Expression[] {
return [o.literal(core.AttributeMarker.ProjectAs), asLiteral(parsedR3Selector)];
}
function interpolate(args: o.Expression[]): o.Expression {
args = args.slice(1); // Ignore the length prefix added for render2
switch (args.length) {
case 3:
return o.importExpr(R3.interpolation1).callFn(args);
case 5:
return o.importExpr(R3.interpolation2).callFn(args);
case 7:
return o.importExpr(R3.interpolation3).callFn(args);
case 9:
return o.importExpr(R3.interpolation4).callFn(args);
case 11:
return o.importExpr(R3.interpolation5).callFn(args);
case 13:
return o.importExpr(R3.interpolation6).callFn(args);
case 15:
return o.importExpr(R3.interpolation7).callFn(args);
case 17:
return o.importExpr(R3.interpolation8).callFn(args);
}
(args.length >= 19 && args.length % 2 == 1) ||
error(`Invalid interpolation argument length ${args.length}`);
return o.importExpr(R3.interpolationV).callFn([o.literalArr(args)]);
}
/**
* Gets the instruction to generate for an interpolated property
* @param interpolation An Interpolation AST
@ -1861,22 +1840,6 @@ function getTextInterpolationExpression(interpolation: Interpolation): o.Externa
}
}
/**
* Gets the number of arguments expected to be passed to a generated instruction in the case of
* interpolation instructions.
* @param interpolation An interpolation ast
*/
function getInterpolationArgsLength(interpolation: Interpolation) {
const {expressions, strings} = interpolation;
if (expressions.length === 1 && strings.length === 2 && strings[0] === '' && strings[1] === '') {
// If the interpolation has one interpolated value, but the prefix and suffix are both empty
// strings, we only pass one argument, to a special instruction like `propertyInterpolate` or
// `textInterpolate`.
return 1;
} else {
return expressions.length + strings.length;
}
}
/**
* Options that can be used to modify how a template is parsed by `parseTemplate()`.
*/

View File

@ -7,6 +7,7 @@
*/
import {ConstantPool} from '../../constant_pool';
import {Interpolation} from '../../expression_parser/ast';
import * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util';
import {splitAtColon} from '../../util';
@ -201,3 +202,20 @@ export function chainedInstruction(
return expression;
}
/**
* Gets the number of arguments expected to be passed to a generated instruction in the case of
* interpolation instructions.
* @param interpolation An interpolation ast
*/
export function getInterpolationArgsLength(interpolation: Interpolation) {
const {expressions, strings} = interpolation;
if (expressions.length === 1 && strings.length === 2 && strings[0] === '' && strings[1] === '') {
// If the interpolation has one interpolated value, but the prefix and suffix are both empty
// strings, we only pass one argument, to a special instruction like `propertyInterpolate` or
// `textInterpolate`.
return 1;
} else {
return expressions.length + strings.length;
}
}