
The `convertActionBinding()` now accepts an optional `baseSourceSpan`, which is the start point of the action expression being converted in the original source code. This is used to compute the original position of the output AST nodes. PR Close #28055
738 lines
28 KiB
TypeScript
738 lines
28 KiB
TypeScript
/**
|
|
* @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 * as cdAst from '../expression_parser/ast';
|
|
import {Identifiers} from '../identifiers';
|
|
import * as o from '../output/output_ast';
|
|
import {ParseSourceSpan} from '../parse_util';
|
|
|
|
export class EventHandlerVars { static event = o.variable('$event'); }
|
|
|
|
export interface LocalResolver { getLocal(name: string): o.Expression|null; }
|
|
|
|
export class ConvertActionBindingResult {
|
|
/**
|
|
* Store statements which are render3 compatible.
|
|
*/
|
|
render3Stmts: o.Statement[];
|
|
constructor(
|
|
/**
|
|
* Render2 compatible statements,
|
|
*/
|
|
public stmts: o.Statement[],
|
|
/**
|
|
* Variable name used with render2 compatible statements.
|
|
*/
|
|
public allowDefault: o.ReadVarExpr) {
|
|
/**
|
|
* This is bit of a hack. It converts statements which render2 expects to statements which are
|
|
* expected by render3.
|
|
*
|
|
* Example: `<div click="doSomething($event)">` will generate:
|
|
*
|
|
* Render3:
|
|
* ```
|
|
* const pd_b:any = ((<any>ctx.doSomething($event)) !== false);
|
|
* return pd_b;
|
|
* ```
|
|
*
|
|
* but render2 expects:
|
|
* ```
|
|
* return ctx.doSomething($event);
|
|
* ```
|
|
*/
|
|
// TODO(misko): remove this hack once we no longer support ViewEngine.
|
|
this.render3Stmts = stmts.map((statement: o.Statement) => {
|
|
if (statement instanceof o.DeclareVarStmt && statement.name == allowDefault.name &&
|
|
statement.value instanceof o.BinaryOperatorExpr) {
|
|
const lhs = statement.value.lhs as o.CastExpr;
|
|
return new o.ReturnStatement(lhs.value);
|
|
}
|
|
return statement;
|
|
});
|
|
}
|
|
}
|
|
|
|
export type InterpolationFunction = (args: o.Expression[]) => o.Expression;
|
|
|
|
/**
|
|
* Converts the given expression AST into an executable output AST, assuming the expression is
|
|
* used in an action binding (e.g. an event handler).
|
|
*/
|
|
export function convertActionBinding(
|
|
localResolver: LocalResolver | null, implicitReceiver: o.Expression, action: cdAst.AST,
|
|
bindingId: string, interpolationFunction?: InterpolationFunction,
|
|
baseSourceSpan?: ParseSourceSpan): ConvertActionBindingResult {
|
|
if (!localResolver) {
|
|
localResolver = new DefaultLocalResolver();
|
|
}
|
|
const actionWithoutBuiltins = convertPropertyBindingBuiltins(
|
|
{
|
|
createLiteralArrayConverter: (argCount: number) => {
|
|
// Note: no caching for literal arrays in actions.
|
|
return (args: o.Expression[]) => o.literalArr(args);
|
|
},
|
|
createLiteralMapConverter: (keys: {key: string, quoted: boolean}[]) => {
|
|
// Note: no caching for literal maps in actions.
|
|
return (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) => {
|
|
throw new Error(`Illegal State: Actions are not allowed to contain pipes. Pipe: ${name}`);
|
|
}
|
|
},
|
|
action);
|
|
|
|
const visitor = new _AstToIrVisitor(
|
|
localResolver, implicitReceiver, bindingId, interpolationFunction, baseSourceSpan);
|
|
const actionStmts: o.Statement[] = [];
|
|
flattenStatements(actionWithoutBuiltins.visit(visitor, _Mode.Statement), actionStmts);
|
|
prependTemporaryDecls(visitor.temporaryCount, bindingId, actionStmts);
|
|
const lastIndex = actionStmts.length - 1;
|
|
let preventDefaultVar: o.ReadVarExpr = null !;
|
|
if (lastIndex >= 0) {
|
|
const lastStatement = actionStmts[lastIndex];
|
|
const returnExpr = convertStmtIntoExpression(lastStatement);
|
|
if (returnExpr) {
|
|
// Note: We need to cast the result of the method call to dynamic,
|
|
// as it might be a void method!
|
|
preventDefaultVar = createPreventDefaultVar(bindingId);
|
|
actionStmts[lastIndex] =
|
|
preventDefaultVar.set(returnExpr.cast(o.DYNAMIC_TYPE).notIdentical(o.literal(false)))
|
|
.toDeclStmt(null, [o.StmtModifier.Final]);
|
|
}
|
|
}
|
|
return new ConvertActionBindingResult(actionStmts, preventDefaultVar);
|
|
}
|
|
|
|
export interface BuiltinConverter { (args: o.Expression[]): o.Expression; }
|
|
|
|
export interface BuiltinConverterFactory {
|
|
createLiteralArrayConverter(argCount: number): BuiltinConverter;
|
|
createLiteralMapConverter(keys: {key: string, quoted: boolean}[]): BuiltinConverter;
|
|
createPipeConverter(name: string, argCount: number): BuiltinConverter;
|
|
}
|
|
|
|
export function convertPropertyBindingBuiltins(
|
|
converterFactory: BuiltinConverterFactory, ast: cdAst.AST): cdAst.AST {
|
|
return convertBuiltins(converterFactory, ast);
|
|
}
|
|
|
|
export class ConvertPropertyBindingResult {
|
|
constructor(public stmts: o.Statement[], public currValExpr: o.Expression) {}
|
|
}
|
|
|
|
export enum BindingForm {
|
|
// The general form of binding expression, supports all expressions.
|
|
General,
|
|
|
|
// Try to generate a simple binding (no temporaries or statements)
|
|
// otherwise generate a general binding
|
|
TrySimple,
|
|
}
|
|
|
|
/**
|
|
* Converts the given expression AST into an executable output AST, assuming the expression
|
|
* is used in property binding. The expression has to be preprocessed via
|
|
* `convertPropertyBindingBuiltins`.
|
|
*/
|
|
export function convertPropertyBinding(
|
|
localResolver: LocalResolver | null, implicitReceiver: o.Expression,
|
|
expressionWithoutBuiltins: cdAst.AST, bindingId: string, form: BindingForm,
|
|
interpolationFunction?: InterpolationFunction): ConvertPropertyBindingResult {
|
|
if (!localResolver) {
|
|
localResolver = new DefaultLocalResolver();
|
|
}
|
|
const currValExpr = createCurrValueExpr(bindingId);
|
|
const stmts: o.Statement[] = [];
|
|
const visitor =
|
|
new _AstToIrVisitor(localResolver, implicitReceiver, bindingId, interpolationFunction);
|
|
const outputExpr: o.Expression = expressionWithoutBuiltins.visit(visitor, _Mode.Expression);
|
|
|
|
if (visitor.temporaryCount) {
|
|
for (let i = 0; i < visitor.temporaryCount; i++) {
|
|
stmts.push(temporaryDeclaration(bindingId, i));
|
|
}
|
|
} else if (form == BindingForm.TrySimple) {
|
|
return new ConvertPropertyBindingResult([], outputExpr);
|
|
}
|
|
|
|
stmts.push(currValExpr.set(outputExpr).toDeclStmt(o.DYNAMIC_TYPE, [o.StmtModifier.Final]));
|
|
return new ConvertPropertyBindingResult(stmts, currValExpr);
|
|
}
|
|
|
|
function convertBuiltins(converterFactory: BuiltinConverterFactory, ast: cdAst.AST): cdAst.AST {
|
|
const visitor = new _BuiltinAstConverter(converterFactory);
|
|
return ast.visit(visitor);
|
|
}
|
|
|
|
function temporaryName(bindingId: string, temporaryNumber: number): string {
|
|
return `tmp_${bindingId}_${temporaryNumber}`;
|
|
}
|
|
|
|
export function temporaryDeclaration(bindingId: string, temporaryNumber: number): o.Statement {
|
|
return new o.DeclareVarStmt(temporaryName(bindingId, temporaryNumber), o.NULL_EXPR);
|
|
}
|
|
|
|
function prependTemporaryDecls(
|
|
temporaryCount: number, bindingId: string, statements: o.Statement[]) {
|
|
for (let i = temporaryCount - 1; i >= 0; i--) {
|
|
statements.unshift(temporaryDeclaration(bindingId, i));
|
|
}
|
|
}
|
|
|
|
enum _Mode {
|
|
Statement,
|
|
Expression
|
|
}
|
|
|
|
function ensureStatementMode(mode: _Mode, ast: cdAst.AST) {
|
|
if (mode !== _Mode.Statement) {
|
|
throw new Error(`Expected a statement, but saw ${ast}`);
|
|
}
|
|
}
|
|
|
|
function ensureExpressionMode(mode: _Mode, ast: cdAst.AST) {
|
|
if (mode !== _Mode.Expression) {
|
|
throw new Error(`Expected an expression, but saw ${ast}`);
|
|
}
|
|
}
|
|
|
|
function convertToStatementIfNeeded(mode: _Mode, expr: o.Expression): o.Expression|o.Statement {
|
|
if (mode === _Mode.Statement) {
|
|
return expr.toStmt();
|
|
} else {
|
|
return expr;
|
|
}
|
|
}
|
|
|
|
class _BuiltinAstConverter extends cdAst.AstTransformer {
|
|
constructor(private _converterFactory: BuiltinConverterFactory) { super(); }
|
|
visitPipe(ast: cdAst.BindingPipe, context: any): any {
|
|
const args = [ast.exp, ...ast.args].map(ast => ast.visit(this, context));
|
|
return new BuiltinFunctionCall(
|
|
ast.span, args, this._converterFactory.createPipeConverter(ast.name, args.length));
|
|
}
|
|
visitLiteralArray(ast: cdAst.LiteralArray, context: any): any {
|
|
const args = ast.expressions.map(ast => ast.visit(this, context));
|
|
return new BuiltinFunctionCall(
|
|
ast.span, args, this._converterFactory.createLiteralArrayConverter(ast.expressions.length));
|
|
}
|
|
visitLiteralMap(ast: cdAst.LiteralMap, context: any): any {
|
|
const args = ast.values.map(ast => ast.visit(this, context));
|
|
|
|
return new BuiltinFunctionCall(
|
|
ast.span, args, this._converterFactory.createLiteralMapConverter(ast.keys));
|
|
}
|
|
}
|
|
|
|
class _AstToIrVisitor implements cdAst.AstVisitor {
|
|
private _nodeMap = new Map<cdAst.AST, cdAst.AST>();
|
|
private _resultMap = new Map<cdAst.AST, o.Expression>();
|
|
private _currentTemporary: number = 0;
|
|
public temporaryCount: number = 0;
|
|
|
|
constructor(
|
|
private _localResolver: LocalResolver, private _implicitReceiver: o.Expression,
|
|
private bindingId: string, private interpolationFunction: InterpolationFunction|undefined,
|
|
private baseSourceSpan?: ParseSourceSpan) {}
|
|
|
|
visitBinary(ast: cdAst.Binary, mode: _Mode): any {
|
|
let op: o.BinaryOperator;
|
|
switch (ast.operation) {
|
|
case '+':
|
|
op = o.BinaryOperator.Plus;
|
|
break;
|
|
case '-':
|
|
op = o.BinaryOperator.Minus;
|
|
break;
|
|
case '*':
|
|
op = o.BinaryOperator.Multiply;
|
|
break;
|
|
case '/':
|
|
op = o.BinaryOperator.Divide;
|
|
break;
|
|
case '%':
|
|
op = o.BinaryOperator.Modulo;
|
|
break;
|
|
case '&&':
|
|
op = o.BinaryOperator.And;
|
|
break;
|
|
case '||':
|
|
op = o.BinaryOperator.Or;
|
|
break;
|
|
case '==':
|
|
op = o.BinaryOperator.Equals;
|
|
break;
|
|
case '!=':
|
|
op = o.BinaryOperator.NotEquals;
|
|
break;
|
|
case '===':
|
|
op = o.BinaryOperator.Identical;
|
|
break;
|
|
case '!==':
|
|
op = o.BinaryOperator.NotIdentical;
|
|
break;
|
|
case '<':
|
|
op = o.BinaryOperator.Lower;
|
|
break;
|
|
case '>':
|
|
op = o.BinaryOperator.Bigger;
|
|
break;
|
|
case '<=':
|
|
op = o.BinaryOperator.LowerEquals;
|
|
break;
|
|
case '>=':
|
|
op = o.BinaryOperator.BiggerEquals;
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported operation ${ast.operation}`);
|
|
}
|
|
|
|
return convertToStatementIfNeeded(
|
|
mode,
|
|
new o.BinaryOperatorExpr(
|
|
op, this._visit(ast.left, _Mode.Expression), this._visit(ast.right, _Mode.Expression),
|
|
undefined, this.convertSourceSpan(ast.span)));
|
|
}
|
|
|
|
visitChain(ast: cdAst.Chain, mode: _Mode): any {
|
|
ensureStatementMode(mode, ast);
|
|
return this.visitAll(ast.expressions, mode);
|
|
}
|
|
|
|
visitConditional(ast: cdAst.Conditional, mode: _Mode): any {
|
|
const value: o.Expression = this._visit(ast.condition, _Mode.Expression);
|
|
return convertToStatementIfNeeded(
|
|
mode, value.conditional(
|
|
this._visit(ast.trueExp, _Mode.Expression),
|
|
this._visit(ast.falseExp, _Mode.Expression), this.convertSourceSpan(ast.span)));
|
|
}
|
|
|
|
visitPipe(ast: cdAst.BindingPipe, mode: _Mode): any {
|
|
throw new Error(
|
|
`Illegal state: Pipes should have been converted into functions. Pipe: ${ast.name}`);
|
|
}
|
|
|
|
visitFunctionCall(ast: cdAst.FunctionCall, mode: _Mode): any {
|
|
const convertedArgs = this.visitAll(ast.args, _Mode.Expression);
|
|
let fnResult: o.Expression;
|
|
if (ast instanceof BuiltinFunctionCall) {
|
|
fnResult = ast.converter(convertedArgs);
|
|
} else {
|
|
fnResult = this._visit(ast.target !, _Mode.Expression)
|
|
.callFn(convertedArgs, this.convertSourceSpan(ast.span));
|
|
}
|
|
return convertToStatementIfNeeded(mode, fnResult);
|
|
}
|
|
|
|
visitImplicitReceiver(ast: cdAst.ImplicitReceiver, mode: _Mode): any {
|
|
ensureExpressionMode(mode, ast);
|
|
return this._implicitReceiver;
|
|
}
|
|
|
|
visitInterpolation(ast: cdAst.Interpolation, mode: _Mode): any {
|
|
ensureExpressionMode(mode, ast);
|
|
const args = [o.literal(ast.expressions.length)];
|
|
for (let i = 0; i < ast.strings.length - 1; i++) {
|
|
args.push(o.literal(ast.strings[i]));
|
|
args.push(this._visit(ast.expressions[i], _Mode.Expression));
|
|
}
|
|
args.push(o.literal(ast.strings[ast.strings.length - 1]));
|
|
|
|
if (this.interpolationFunction) {
|
|
return this.interpolationFunction(args);
|
|
}
|
|
return ast.expressions.length <= 9 ?
|
|
o.importExpr(Identifiers.inlineInterpolate).callFn(args) :
|
|
o.importExpr(Identifiers.interpolate).callFn([
|
|
args[0], o.literalArr(args.slice(1), undefined, this.convertSourceSpan(ast.span))
|
|
]);
|
|
}
|
|
|
|
visitKeyedRead(ast: cdAst.KeyedRead, mode: _Mode): any {
|
|
const leftMostSafe = this.leftMostSafeNode(ast);
|
|
if (leftMostSafe) {
|
|
return this.convertSafeAccess(ast, leftMostSafe, mode);
|
|
} else {
|
|
return convertToStatementIfNeeded(
|
|
mode, this._visit(ast.obj, _Mode.Expression).key(this._visit(ast.key, _Mode.Expression)));
|
|
}
|
|
}
|
|
|
|
visitKeyedWrite(ast: cdAst.KeyedWrite, mode: _Mode): any {
|
|
const obj: o.Expression = this._visit(ast.obj, _Mode.Expression);
|
|
const key: o.Expression = this._visit(ast.key, _Mode.Expression);
|
|
const value: o.Expression = this._visit(ast.value, _Mode.Expression);
|
|
return convertToStatementIfNeeded(mode, obj.key(key).set(value));
|
|
}
|
|
|
|
visitLiteralArray(ast: cdAst.LiteralArray, mode: _Mode): any {
|
|
throw new Error(`Illegal State: literal arrays should have been converted into functions`);
|
|
}
|
|
|
|
visitLiteralMap(ast: cdAst.LiteralMap, mode: _Mode): any {
|
|
throw new Error(`Illegal State: literal maps should have been converted into functions`);
|
|
}
|
|
|
|
visitLiteralPrimitive(ast: cdAst.LiteralPrimitive, mode: _Mode): any {
|
|
// For literal values of null, undefined, true, or false allow type interference
|
|
// to infer the type.
|
|
const type =
|
|
ast.value === null || ast.value === undefined || ast.value === true || ast.value === true ?
|
|
o.INFERRED_TYPE :
|
|
undefined;
|
|
return convertToStatementIfNeeded(
|
|
mode, o.literal(ast.value, type, this.convertSourceSpan(ast.span)));
|
|
}
|
|
|
|
private _getLocal(name: string): o.Expression|null { return this._localResolver.getLocal(name); }
|
|
|
|
visitMethodCall(ast: cdAst.MethodCall, mode: _Mode): any {
|
|
if (ast.receiver instanceof cdAst.ImplicitReceiver && ast.name == '$any') {
|
|
const args = this.visitAll(ast.args, _Mode.Expression) as any[];
|
|
if (args.length != 1) {
|
|
throw new Error(
|
|
`Invalid call to $any, expected 1 argument but received ${args.length || 'none'}`);
|
|
}
|
|
return (args[0] as o.Expression).cast(o.DYNAMIC_TYPE, this.convertSourceSpan(ast.span));
|
|
}
|
|
|
|
const leftMostSafe = this.leftMostSafeNode(ast);
|
|
if (leftMostSafe) {
|
|
return this.convertSafeAccess(ast, leftMostSafe, mode);
|
|
} else {
|
|
const args = this.visitAll(ast.args, _Mode.Expression);
|
|
let result: any = null;
|
|
const receiver = this._visit(ast.receiver, _Mode.Expression);
|
|
if (receiver === this._implicitReceiver) {
|
|
const varExpr = this._getLocal(ast.name);
|
|
if (varExpr) {
|
|
result = varExpr.callFn(args);
|
|
}
|
|
}
|
|
if (result == null) {
|
|
result = receiver.callMethod(ast.name, args, this.convertSourceSpan(ast.span));
|
|
}
|
|
return convertToStatementIfNeeded(mode, result);
|
|
}
|
|
}
|
|
|
|
visitPrefixNot(ast: cdAst.PrefixNot, mode: _Mode): any {
|
|
return convertToStatementIfNeeded(mode, o.not(this._visit(ast.expression, _Mode.Expression)));
|
|
}
|
|
|
|
visitNonNullAssert(ast: cdAst.NonNullAssert, mode: _Mode): any {
|
|
return convertToStatementIfNeeded(
|
|
mode, o.assertNotNull(this._visit(ast.expression, _Mode.Expression)));
|
|
}
|
|
|
|
visitPropertyRead(ast: cdAst.PropertyRead, mode: _Mode): any {
|
|
const leftMostSafe = this.leftMostSafeNode(ast);
|
|
if (leftMostSafe) {
|
|
return this.convertSafeAccess(ast, leftMostSafe, mode);
|
|
} else {
|
|
let result: any = null;
|
|
const receiver = this._visit(ast.receiver, _Mode.Expression);
|
|
if (receiver === this._implicitReceiver) {
|
|
result = this._getLocal(ast.name);
|
|
}
|
|
if (result == null) {
|
|
result = receiver.prop(ast.name);
|
|
}
|
|
return convertToStatementIfNeeded(mode, result);
|
|
}
|
|
}
|
|
|
|
visitPropertyWrite(ast: cdAst.PropertyWrite, mode: _Mode): any {
|
|
const receiver: o.Expression = this._visit(ast.receiver, _Mode.Expression);
|
|
|
|
let varExpr: o.ReadPropExpr|null = null;
|
|
if (receiver === this._implicitReceiver) {
|
|
const localExpr = this._getLocal(ast.name);
|
|
if (localExpr) {
|
|
if (localExpr instanceof o.ReadPropExpr) {
|
|
// If the local variable is a property read expression, it's a reference
|
|
// to a 'context.property' value and will be used as the target of the
|
|
// write expression.
|
|
varExpr = localExpr;
|
|
} else {
|
|
// Otherwise it's an error.
|
|
throw new Error('Cannot assign to a reference or variable!');
|
|
}
|
|
}
|
|
}
|
|
// If no local expression could be produced, use the original receiver's
|
|
// property as the target.
|
|
if (varExpr === null) {
|
|
varExpr = receiver.prop(ast.name);
|
|
}
|
|
return convertToStatementIfNeeded(mode, varExpr.set(this._visit(ast.value, _Mode.Expression)));
|
|
}
|
|
|
|
visitSafePropertyRead(ast: cdAst.SafePropertyRead, mode: _Mode): any {
|
|
return this.convertSafeAccess(ast, this.leftMostSafeNode(ast), mode);
|
|
}
|
|
|
|
visitSafeMethodCall(ast: cdAst.SafeMethodCall, mode: _Mode): any {
|
|
return this.convertSafeAccess(ast, this.leftMostSafeNode(ast), mode);
|
|
}
|
|
|
|
visitAll(asts: cdAst.AST[], mode: _Mode): any { return asts.map(ast => this._visit(ast, mode)); }
|
|
|
|
visitQuote(ast: cdAst.Quote, mode: _Mode): any {
|
|
throw new Error(`Quotes are not supported for evaluation!
|
|
Statement: ${ast.uninterpretedExpression} located at ${ast.location}`);
|
|
}
|
|
|
|
private _visit(ast: cdAst.AST, mode: _Mode): any {
|
|
const result = this._resultMap.get(ast);
|
|
if (result) return result;
|
|
return (this._nodeMap.get(ast) || ast).visit(this, mode);
|
|
}
|
|
|
|
private convertSafeAccess(
|
|
ast: cdAst.AST, leftMostSafe: cdAst.SafeMethodCall|cdAst.SafePropertyRead, mode: _Mode): any {
|
|
// If the expression contains a safe access node on the left it needs to be converted to
|
|
// an expression that guards the access to the member by checking the receiver for blank. As
|
|
// execution proceeds from left to right, the left most part of the expression must be guarded
|
|
// first but, because member access is left associative, the right side of the expression is at
|
|
// the top of the AST. The desired result requires lifting a copy of the the left part of the
|
|
// expression up to test it for blank before generating the unguarded version.
|
|
|
|
// Consider, for example the following expression: a?.b.c?.d.e
|
|
|
|
// This results in the ast:
|
|
// .
|
|
// / \
|
|
// ?. e
|
|
// / \
|
|
// . d
|
|
// / \
|
|
// ?. c
|
|
// / \
|
|
// a b
|
|
|
|
// The following tree should be generated:
|
|
//
|
|
// /---- ? ----\
|
|
// / | \
|
|
// a /--- ? ---\ null
|
|
// / | \
|
|
// . . null
|
|
// / \ / \
|
|
// . c . e
|
|
// / \ / \
|
|
// a b , d
|
|
// / \
|
|
// . c
|
|
// / \
|
|
// a b
|
|
//
|
|
// Notice that the first guard condition is the left hand of the left most safe access node
|
|
// which comes in as leftMostSafe to this routine.
|
|
|
|
let guardedExpression = this._visit(leftMostSafe.receiver, _Mode.Expression);
|
|
let temporary: o.ReadVarExpr = undefined !;
|
|
if (this.needsTemporary(leftMostSafe.receiver)) {
|
|
// If the expression has method calls or pipes then we need to save the result into a
|
|
// temporary variable to avoid calling stateful or impure code more than once.
|
|
temporary = this.allocateTemporary();
|
|
|
|
// Preserve the result in the temporary variable
|
|
guardedExpression = temporary.set(guardedExpression);
|
|
|
|
// Ensure all further references to the guarded expression refer to the temporary instead.
|
|
this._resultMap.set(leftMostSafe.receiver, temporary);
|
|
}
|
|
const condition = guardedExpression.isBlank();
|
|
|
|
// Convert the ast to an unguarded access to the receiver's member. The map will substitute
|
|
// leftMostNode with its unguarded version in the call to `this.visit()`.
|
|
if (leftMostSafe instanceof cdAst.SafeMethodCall) {
|
|
this._nodeMap.set(
|
|
leftMostSafe,
|
|
new cdAst.MethodCall(
|
|
leftMostSafe.span, leftMostSafe.receiver, leftMostSafe.name, leftMostSafe.args));
|
|
} else {
|
|
this._nodeMap.set(
|
|
leftMostSafe,
|
|
new cdAst.PropertyRead(leftMostSafe.span, leftMostSafe.receiver, leftMostSafe.name));
|
|
}
|
|
|
|
// Recursively convert the node now without the guarded member access.
|
|
const access = this._visit(ast, _Mode.Expression);
|
|
|
|
// Remove the mapping. This is not strictly required as the converter only traverses each node
|
|
// once but is safer if the conversion is changed to traverse the nodes more than once.
|
|
this._nodeMap.delete(leftMostSafe);
|
|
|
|
// If we allocated a temporary, release it.
|
|
if (temporary) {
|
|
this.releaseTemporary(temporary);
|
|
}
|
|
|
|
// Produce the conditional
|
|
return convertToStatementIfNeeded(mode, condition.conditional(o.literal(null), access));
|
|
}
|
|
|
|
// Given a expression of the form a?.b.c?.d.e the the left most safe node is
|
|
// the (a?.b). The . and ?. are left associative thus can be rewritten as:
|
|
// ((((a?.c).b).c)?.d).e. This returns the most deeply nested safe read or
|
|
// safe method call as this needs be transform initially to:
|
|
// a == null ? null : a.c.b.c?.d.e
|
|
// then to:
|
|
// a == null ? null : a.b.c == null ? null : a.b.c.d.e
|
|
private leftMostSafeNode(ast: cdAst.AST): cdAst.SafePropertyRead|cdAst.SafeMethodCall {
|
|
const visit = (visitor: cdAst.AstVisitor, ast: cdAst.AST): any => {
|
|
return (this._nodeMap.get(ast) || ast).visit(visitor);
|
|
};
|
|
return ast.visit({
|
|
visitBinary(ast: cdAst.Binary) { return null; },
|
|
visitChain(ast: cdAst.Chain) { return null; },
|
|
visitConditional(ast: cdAst.Conditional) { return null; },
|
|
visitFunctionCall(ast: cdAst.FunctionCall) { return null; },
|
|
visitImplicitReceiver(ast: cdAst.ImplicitReceiver) { return null; },
|
|
visitInterpolation(ast: cdAst.Interpolation) { return null; },
|
|
visitKeyedRead(ast: cdAst.KeyedRead) { return visit(this, ast.obj); },
|
|
visitKeyedWrite(ast: cdAst.KeyedWrite) { return null; },
|
|
visitLiteralArray(ast: cdAst.LiteralArray) { return null; },
|
|
visitLiteralMap(ast: cdAst.LiteralMap) { return null; },
|
|
visitLiteralPrimitive(ast: cdAst.LiteralPrimitive) { return null; },
|
|
visitMethodCall(ast: cdAst.MethodCall) { return visit(this, ast.receiver); },
|
|
visitPipe(ast: cdAst.BindingPipe) { return null; },
|
|
visitPrefixNot(ast: cdAst.PrefixNot) { return null; },
|
|
visitNonNullAssert(ast: cdAst.NonNullAssert) { return null; },
|
|
visitPropertyRead(ast: cdAst.PropertyRead) { return visit(this, ast.receiver); },
|
|
visitPropertyWrite(ast: cdAst.PropertyWrite) { return null; },
|
|
visitQuote(ast: cdAst.Quote) { return null; },
|
|
visitSafeMethodCall(ast: cdAst.SafeMethodCall) { return visit(this, ast.receiver) || ast; },
|
|
visitSafePropertyRead(ast: cdAst.SafePropertyRead) {
|
|
return visit(this, ast.receiver) || ast;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Returns true of the AST includes a method or a pipe indicating that, if the
|
|
// expression is used as the target of a safe property or method access then
|
|
// the expression should be stored into a temporary variable.
|
|
private needsTemporary(ast: cdAst.AST): boolean {
|
|
const visit = (visitor: cdAst.AstVisitor, ast: cdAst.AST): boolean => {
|
|
return ast && (this._nodeMap.get(ast) || ast).visit(visitor);
|
|
};
|
|
const visitSome = (visitor: cdAst.AstVisitor, ast: cdAst.AST[]): boolean => {
|
|
return ast.some(ast => visit(visitor, ast));
|
|
};
|
|
return ast.visit({
|
|
visitBinary(ast: cdAst.Binary):
|
|
boolean{return visit(this, ast.left) || visit(this, ast.right);},
|
|
visitChain(ast: cdAst.Chain) { return false; },
|
|
visitConditional(ast: cdAst.Conditional):
|
|
boolean{return visit(this, ast.condition) || visit(this, ast.trueExp) ||
|
|
visit(this, ast.falseExp);},
|
|
visitFunctionCall(ast: cdAst.FunctionCall) { return true; },
|
|
visitImplicitReceiver(ast: cdAst.ImplicitReceiver) { return false; },
|
|
visitInterpolation(ast: cdAst.Interpolation) { return visitSome(this, ast.expressions); },
|
|
visitKeyedRead(ast: cdAst.KeyedRead) { return false; },
|
|
visitKeyedWrite(ast: cdAst.KeyedWrite) { return false; },
|
|
visitLiteralArray(ast: cdAst.LiteralArray) { return true; },
|
|
visitLiteralMap(ast: cdAst.LiteralMap) { return true; },
|
|
visitLiteralPrimitive(ast: cdAst.LiteralPrimitive) { return false; },
|
|
visitMethodCall(ast: cdAst.MethodCall) { return true; },
|
|
visitPipe(ast: cdAst.BindingPipe) { return true; },
|
|
visitPrefixNot(ast: cdAst.PrefixNot) { return visit(this, ast.expression); },
|
|
visitNonNullAssert(ast: cdAst.PrefixNot) { return visit(this, ast.expression); },
|
|
visitPropertyRead(ast: cdAst.PropertyRead) { return false; },
|
|
visitPropertyWrite(ast: cdAst.PropertyWrite) { return false; },
|
|
visitQuote(ast: cdAst.Quote) { return false; },
|
|
visitSafeMethodCall(ast: cdAst.SafeMethodCall) { return true; },
|
|
visitSafePropertyRead(ast: cdAst.SafePropertyRead) { return false; }
|
|
});
|
|
}
|
|
|
|
private allocateTemporary(): o.ReadVarExpr {
|
|
const tempNumber = this._currentTemporary++;
|
|
this.temporaryCount = Math.max(this._currentTemporary, this.temporaryCount);
|
|
return new o.ReadVarExpr(temporaryName(this.bindingId, tempNumber));
|
|
}
|
|
|
|
private releaseTemporary(temporary: o.ReadVarExpr) {
|
|
this._currentTemporary--;
|
|
if (temporary.name != temporaryName(this.bindingId, this._currentTemporary)) {
|
|
throw new Error(`Temporary ${temporary.name} released out of order`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates an absolute `ParseSourceSpan` from the relative `ParseSpan`.
|
|
*
|
|
* `ParseSpan` objects are relative to the start of the expression.
|
|
* This method converts these to full `ParseSourceSpan` objects that
|
|
* show where the span is within the overall source file.
|
|
*
|
|
* @param span the relative span to convert.
|
|
* @returns a `ParseSourceSpan` for the the given span or null if no
|
|
* `baseSourceSpan` was provided to this class.
|
|
*/
|
|
private convertSourceSpan(span: cdAst.ParseSpan) {
|
|
if (this.baseSourceSpan) {
|
|
const start = this.baseSourceSpan.start.moveBy(span.start);
|
|
const end = this.baseSourceSpan.start.moveBy(span.end);
|
|
return new ParseSourceSpan(start, end);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
function flattenStatements(arg: any, output: o.Statement[]) {
|
|
if (Array.isArray(arg)) {
|
|
(<any[]>arg).forEach((entry) => flattenStatements(entry, output));
|
|
} else {
|
|
output.push(arg);
|
|
}
|
|
}
|
|
|
|
class DefaultLocalResolver implements LocalResolver {
|
|
getLocal(name: string): o.Expression|null {
|
|
if (name === EventHandlerVars.event.name) {
|
|
return EventHandlerVars.event;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function createCurrValueExpr(bindingId: string): o.ReadVarExpr {
|
|
return o.variable(`currVal_${bindingId}`); // fix syntax highlighting: `
|
|
}
|
|
|
|
function createPreventDefaultVar(bindingId: string): o.ReadVarExpr {
|
|
return o.variable(`pd_${bindingId}`);
|
|
}
|
|
|
|
function convertStmtIntoExpression(stmt: o.Statement): o.Expression|null {
|
|
if (stmt instanceof o.ExpressionStatement) {
|
|
return stmt.expr;
|
|
} else if (stmt instanceof o.ReturnStatement) {
|
|
return stmt.value;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export class BuiltinFunctionCall extends cdAst.FunctionCall {
|
|
constructor(span: cdAst.ParseSpan, public args: cdAst.AST[], public converter: BuiltinConverter) {
|
|
super(span, null, args);
|
|
}
|
|
}
|