2018-04-24 11:34:11 -07:00
|
|
|
/**
|
|
|
|
* @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 {ConstantPool} from '../../constant_pool';
|
2019-06-27 18:57:09 +02:00
|
|
|
import {Interpolation} from '../../expression_parser/ast';
|
2018-04-24 11:34:11 -07:00
|
|
|
import * as o from '../../output/output_ast';
|
2019-06-27 20:23:15 +02:00
|
|
|
import {ParseSourceSpan} from '../../parse_util';
|
2018-11-30 17:45:04 +01:00
|
|
|
import {splitAtColon} from '../../util';
|
2018-04-24 11:34:11 -07:00
|
|
|
import * as t from '../r3_ast';
|
2019-06-27 20:23:15 +02:00
|
|
|
|
2018-04-24 11:34:11 -07:00
|
|
|
import {R3QueryMetadata} from './api';
|
2018-10-18 10:08:51 -07:00
|
|
|
import {isI18nAttribute} from './i18n/util';
|
2018-04-24 11:34:11 -07:00
|
|
|
|
2019-06-27 20:23:15 +02:00
|
|
|
|
2019-02-21 21:33:05 -08:00
|
|
|
/**
|
|
|
|
* Checks whether an object key contains potentially unsafe chars, thus the key should be wrapped in
|
|
|
|
* quotes. Note: we do not wrap all keys into quotes, as it may have impact on minification and may
|
|
|
|
* bot work in some cases when object keys are mangled by minifier.
|
|
|
|
*
|
|
|
|
* TODO(FW-1136): this is a temporary solution, we need to come up with a better way of working with
|
|
|
|
* inputs that contain potentially unsafe chars.
|
|
|
|
*/
|
2019-06-19 21:28:50 +02:00
|
|
|
const UNSAFE_OBJECT_KEY_NAME_REGEXP = /[-.]/;
|
2019-02-21 21:33:05 -08:00
|
|
|
|
2018-04-24 11:34:11 -07:00
|
|
|
/** Name of the temporary to use during data binding */
|
|
|
|
export const TEMPORARY_NAME = '_t';
|
|
|
|
|
|
|
|
/** Name of the context parameter passed into a template function */
|
|
|
|
export const CONTEXT_NAME = 'ctx';
|
|
|
|
|
|
|
|
/** Name of the RenderFlag passed into a template function */
|
|
|
|
export const RENDER_FLAGS = 'rf';
|
|
|
|
|
|
|
|
/** The prefix reference variables */
|
|
|
|
export const REFERENCE_PREFIX = '_r';
|
|
|
|
|
|
|
|
/** The name of the implicit context reference */
|
|
|
|
export const IMPLICIT_REFERENCE = '$implicit';
|
|
|
|
|
2018-09-26 13:19:04 -07:00
|
|
|
/** Non bindable attribute name **/
|
|
|
|
export const NON_BINDABLE_ATTR = 'ngNonBindable';
|
|
|
|
|
2018-04-24 11:34:11 -07:00
|
|
|
/**
|
|
|
|
* Creates an allocator for a temporary variable.
|
|
|
|
*
|
|
|
|
* A variable declaration is added to the statements the first time the allocator is invoked.
|
|
|
|
*/
|
|
|
|
export function temporaryAllocator(statements: o.Statement[], name: string): () => o.ReadVarExpr {
|
|
|
|
let temp: o.ReadVarExpr|null = null;
|
|
|
|
return () => {
|
|
|
|
if (!temp) {
|
|
|
|
statements.push(new o.DeclareVarStmt(TEMPORARY_NAME, undefined, o.DYNAMIC_TYPE));
|
|
|
|
temp = o.variable(name);
|
|
|
|
}
|
|
|
|
return temp;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function unsupported(feature: string): never {
|
|
|
|
if (this) {
|
|
|
|
throw new Error(`Builder ${this.constructor.name} doesn't support ${feature} yet`);
|
|
|
|
}
|
|
|
|
throw new Error(`Feature ${feature} is not supported yet`);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function invalid<T>(arg: o.Expression | o.Statement | t.Node): never {
|
|
|
|
throw new Error(
|
2019-02-12 15:37:20 +00:00
|
|
|
`Invalid state: Visitor ${this.constructor.name} doesn't handle ${arg.constructor.name}`);
|
2018-04-24 11:34:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export function asLiteral(value: any): o.Expression {
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
return o.literalArr(value.map(asLiteral));
|
|
|
|
}
|
|
|
|
return o.literal(value, o.INFERRED_TYPE);
|
|
|
|
}
|
|
|
|
|
2018-12-17 13:17:42 -08:00
|
|
|
export function conditionallyCreateMapObjectLiteral(
|
|
|
|
keys: {[key: string]: string | string[]}, keepDeclared?: boolean): o.Expression|null {
|
2018-04-24 11:34:11 -07:00
|
|
|
if (Object.getOwnPropertyNames(keys).length > 0) {
|
2018-12-17 13:17:42 -08:00
|
|
|
return mapToExpression(keys, keepDeclared);
|
2018-04-24 11:34:11 -07:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-12-17 13:17:42 -08:00
|
|
|
function mapToExpression(
|
|
|
|
map: {[key: string]: string | string[]}, keepDeclared?: boolean): o.Expression {
|
2018-11-30 17:45:04 +01:00
|
|
|
return o.literalMap(Object.getOwnPropertyNames(map).map(key => {
|
2018-12-17 13:17:42 -08:00
|
|
|
// canonical syntax: `dirProp: publicProp`
|
2018-11-30 17:45:04 +01:00
|
|
|
// if there is no `:`, use dirProp = elProp
|
2018-12-17 13:17:42 -08:00
|
|
|
const value = map[key];
|
|
|
|
let declaredName: string;
|
|
|
|
let publicName: string;
|
|
|
|
let minifiedName: string;
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
[publicName, declaredName] = value;
|
|
|
|
} else {
|
|
|
|
[declaredName, publicName] = splitAtColon(key, [key, value]);
|
|
|
|
}
|
|
|
|
minifiedName = declaredName;
|
|
|
|
return {
|
|
|
|
key: minifiedName,
|
2019-02-21 21:33:05 -08:00
|
|
|
// put quotes around keys that contain potentially unsafe characters
|
|
|
|
quoted: UNSAFE_OBJECT_KEY_NAME_REGEXP.test(minifiedName),
|
2018-12-17 13:17:42 -08:00
|
|
|
value: (keepDeclared && publicName !== declaredName) ?
|
|
|
|
o.literalArr([asLiteral(publicName), asLiteral(declaredName)]) :
|
|
|
|
asLiteral(publicName)
|
|
|
|
};
|
2018-11-30 17:45:04 +01:00
|
|
|
}));
|
2018-04-24 11:34:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove trailing null nodes as they are implied.
|
|
|
|
*/
|
|
|
|
export function trimTrailingNulls(parameters: o.Expression[]): o.Expression[] {
|
|
|
|
while (o.isNull(parameters[parameters.length - 1])) {
|
|
|
|
parameters.pop();
|
|
|
|
}
|
|
|
|
return parameters;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getQueryPredicate(
|
|
|
|
query: R3QueryMetadata, constantPool: ConstantPool): o.Expression {
|
|
|
|
if (Array.isArray(query.predicate)) {
|
2018-09-28 17:20:43 -07:00
|
|
|
let predicate: o.Expression[] = [];
|
|
|
|
query.predicate.forEach((selector: string): void => {
|
|
|
|
// Each item in predicates array may contain strings with comma-separated refs
|
|
|
|
// (for ex. 'ref, ref1, ..., refN'), thus we extract individual refs and store them
|
|
|
|
// as separate array entities
|
|
|
|
const selectors = selector.split(',').map(token => o.literal(token.trim()));
|
|
|
|
predicate.push(...selectors);
|
|
|
|
});
|
2018-10-03 13:49:24 -07:00
|
|
|
return constantPool.getConstLiteral(o.literalArr(predicate), true);
|
2018-04-24 11:34:11 -07:00
|
|
|
} else {
|
|
|
|
return query.predicate;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function noop() {}
|
|
|
|
|
|
|
|
export class DefinitionMap {
|
|
|
|
values: {key: string, quoted: boolean, value: o.Expression}[] = [];
|
|
|
|
|
|
|
|
set(key: string, value: o.Expression|null): void {
|
|
|
|
if (value) {
|
|
|
|
this.values.push({key, value, quoted: false});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toLiteralMap(): o.LiteralMapExpr { return o.literalMap(this.values); }
|
|
|
|
}
|
2018-09-21 11:41:37 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Extract a map of properties to values for a given element or template node, which can be used
|
|
|
|
* by the directive matching machinery.
|
|
|
|
*
|
|
|
|
* @param elOrTpl the element or template in question
|
|
|
|
* @return an object set up for directive matching. For attributes on the element/template, this
|
|
|
|
* object maps a property name to its (static) value. For any bindings, this map simply maps the
|
|
|
|
* property name to an empty string.
|
|
|
|
*/
|
|
|
|
export function getAttrsForDirectiveMatching(elOrTpl: t.Element | t.Template):
|
|
|
|
{[name: string]: string} {
|
|
|
|
const attributesMap: {[name: string]: string} = {};
|
|
|
|
|
|
|
|
|
fix(ivy): match microsyntax template directives correctly (#29698)
Previously, Template.templateAttrs was introduced to capture attribute
bindings which originated from microsyntax (e.g. bindings in *ngFor="...").
This means that a Template node can have two different structures, depending
on whether it originated from microsyntax or from a literal <ng-template>.
In the literal case, the node behaves much like an Element node, it has
attributes, inputs, and outputs which determine which directives apply.
In the microsyntax case, though, only the templateAttrs should be used
to determine which directives apply.
Previously, both the t2_binder and the TemplateDefinitionBuilder were using
the wrong set of attributes to match directives - combining the attributes,
inputs, outputs, and templateAttrs of the Template node regardless of its
origin. In the TDB's case this wasn't a problem, since the TDB collects a
global Set of directives used in the template, so it didn't matter whether
the directive was also recognized on the <ng-template>. t2_binder's API
distinguishes between directives on specific nodes, though, so it's more
sensitive to mismatching.
In particular, this showed up as an assertion failure in template type-
checking in certain cases, when a directive was accidentally matched on
a microsyntax template element and also had a binding which referenced a
variable declared in the microsyntax. This resulted in the type-checker
attempting to generate a reference to a variable that didn't exist in that
scope.
The fix is to distinguish between the two cases and select the appropriate
set of attributes to match on accordingly.
Testing strategy: tested in the t2_binder tests.
PR Close #29698
2019-04-04 13:19:38 -07:00
|
|
|
if (elOrTpl instanceof t.Template && elOrTpl.tagName !== 'ng-template') {
|
2019-03-07 08:31:31 +00:00
|
|
|
elOrTpl.templateAttrs.forEach(a => attributesMap[a.name] = '');
|
fix(ivy): match microsyntax template directives correctly (#29698)
Previously, Template.templateAttrs was introduced to capture attribute
bindings which originated from microsyntax (e.g. bindings in *ngFor="...").
This means that a Template node can have two different structures, depending
on whether it originated from microsyntax or from a literal <ng-template>.
In the literal case, the node behaves much like an Element node, it has
attributes, inputs, and outputs which determine which directives apply.
In the microsyntax case, though, only the templateAttrs should be used
to determine which directives apply.
Previously, both the t2_binder and the TemplateDefinitionBuilder were using
the wrong set of attributes to match directives - combining the attributes,
inputs, outputs, and templateAttrs of the Template node regardless of its
origin. In the TDB's case this wasn't a problem, since the TDB collects a
global Set of directives used in the template, so it didn't matter whether
the directive was also recognized on the <ng-template>. t2_binder's API
distinguishes between directives on specific nodes, though, so it's more
sensitive to mismatching.
In particular, this showed up as an assertion failure in template type-
checking in certain cases, when a directive was accidentally matched on
a microsyntax template element and also had a binding which referenced a
variable declared in the microsyntax. This resulted in the type-checker
attempting to generate a reference to a variable that didn't exist in that
scope.
The fix is to distinguish between the two cases and select the appropriate
set of attributes to match on accordingly.
Testing strategy: tested in the t2_binder tests.
PR Close #29698
2019-04-04 13:19:38 -07:00
|
|
|
} else {
|
|
|
|
elOrTpl.attributes.forEach(a => {
|
|
|
|
if (!isI18nAttribute(a.name)) {
|
|
|
|
attributesMap[a.name] = a.value;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
elOrTpl.inputs.forEach(i => { attributesMap[i.name] = ''; });
|
|
|
|
elOrTpl.outputs.forEach(o => { attributesMap[o.name] = ''; });
|
2019-03-07 08:31:31 +00:00
|
|
|
}
|
|
|
|
|
2018-09-21 11:41:37 -07:00
|
|
|
return attributesMap;
|
|
|
|
}
|
2019-06-27 20:23:15 +02:00
|
|
|
|
|
|
|
/** Returns a call expression to a chained instruction, e.g. `property(params[0])(params[1])`. */
|
|
|
|
export function chainedInstruction(
|
|
|
|
reference: o.ExternalReference, calls: o.Expression[][], span?: ParseSourceSpan | null) {
|
|
|
|
let expression = o.importExpr(reference, null, span) as o.Expression;
|
|
|
|
|
|
|
|
if (calls.length > 0) {
|
|
|
|
for (let i = 0; i < calls.length; i++) {
|
|
|
|
expression = expression.callFn(calls[i], span);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Add a blank invocation, in case the `calls` array is empty.
|
|
|
|
expression = expression.callFn([], span);
|
|
|
|
}
|
|
|
|
|
|
|
|
return expression;
|
|
|
|
}
|
2019-06-27 18:57:09 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
}
|