2016-06-23 12:47:54 -04:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 15:08:49 -04:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2016-06-23 12:47:54 -04:00
|
|
|
*
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
|
fix(compiler): preserve this.$event and this.$any accesses in expressions (#39323)
Currently expressions `$event.foo()` and `this.$event.foo()`, as well as `$any(foo)` and
`this.$any(foo)`, are treated as the same expression by the compiler, because `this` is considered
the same implicit receiver as when the receiver is omitted. This introduces the following issues:
1. Any time something called `$any` is used, it'll be stripped away, leaving only the first parameter.
2. If something called `$event` is used anywhere in a template, it'll be preserved as `$event`,
rather than being rewritten to `ctx.$event`, causing the value to undefined at runtime. This
applies to listener, property and text bindings.
These changes resolve the first issue and part of the second one by preserving anything that
is accessed through `this`, even if it's one of the "special" ones like `$any` or `$event`.
Furthermore, these changes only expose the `$event` global variable inside event listeners,
whereas previously it was available everywhere.
Fixes #30278.
PR Close #39323
2020-10-18 11:41:29 -04:00
|
|
|
import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, ThisReceiver, Unary} from '../../../src/expression_parser/ast';
|
2018-04-24 17:22:55 -04:00
|
|
|
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../src/ml_parser/interpolation_config';
|
2015-06-06 05:15:14 -04:00
|
|
|
|
2016-07-06 17:06:47 -04:00
|
|
|
class Unparser implements AstVisitor {
|
2015-06-23 06:46:38 -04:00
|
|
|
private static _quoteRegExp = /"/g;
|
2018-06-18 19:38:33 -04:00
|
|
|
// TODO(issue/24571): remove '!'.
|
2020-04-08 13:14:18 -04:00
|
|
|
private _expression!: string;
|
2018-06-18 19:38:33 -04:00
|
|
|
// TODO(issue/24571): remove '!'.
|
2020-04-08 13:14:18 -04:00
|
|
|
private _interpolationConfig!: InterpolationConfig;
|
2015-06-06 05:15:14 -04:00
|
|
|
|
2016-07-06 17:06:47 -04:00
|
|
|
unparse(ast: AST, interpolationConfig: InterpolationConfig) {
|
2015-06-06 05:15:14 -04:00
|
|
|
this._expression = '';
|
2016-06-20 12:52:41 -04:00
|
|
|
this._interpolationConfig = interpolationConfig;
|
2015-06-06 05:15:14 -04:00
|
|
|
this._visit(ast);
|
|
|
|
return this._expression;
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitPropertyRead(ast: PropertyRead, context: any) {
|
2015-06-06 05:15:14 -04:00
|
|
|
this._visit(ast.receiver);
|
|
|
|
this._expression += ast.receiver instanceof ImplicitReceiver ? `${ast.name}` : `.${ast.name}`;
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitPropertyWrite(ast: PropertyWrite, context: any) {
|
2015-08-12 19:26:21 -04:00
|
|
|
this._visit(ast.receiver);
|
|
|
|
this._expression +=
|
|
|
|
ast.receiver instanceof ImplicitReceiver ? `${ast.name} = ` : `.${ast.name} = `;
|
2015-06-06 05:15:14 -04:00
|
|
|
this._visit(ast.value);
|
|
|
|
}
|
|
|
|
|
2020-07-03 19:52:40 -04:00
|
|
|
visitUnary(ast: Unary, context: any) {
|
|
|
|
this._expression += ast.operator;
|
|
|
|
this._visit(ast.expr);
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitBinary(ast: Binary, context: any) {
|
2015-06-06 05:15:14 -04:00
|
|
|
this._visit(ast.left);
|
|
|
|
this._expression += ` ${ast.operation} `;
|
|
|
|
this._visit(ast.right);
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitChain(ast: Chain, context: any) {
|
2016-11-12 08:08:58 -05:00
|
|
|
const len = ast.expressions.length;
|
2015-06-10 05:11:01 -04:00
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
this._visit(ast.expressions[i]);
|
|
|
|
this._expression += i == len - 1 ? ';' : '; ';
|
|
|
|
}
|
2015-06-06 05:15:14 -04:00
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitConditional(ast: Conditional, context: any) {
|
2015-06-06 05:15:14 -04:00
|
|
|
this._visit(ast.condition);
|
|
|
|
this._expression += ' ? ';
|
|
|
|
this._visit(ast.trueExp);
|
|
|
|
this._expression += ' : ';
|
|
|
|
this._visit(ast.falseExp);
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitPipe(ast: BindingPipe, context: any) {
|
2015-06-04 13:06:09 -04:00
|
|
|
this._expression += '(';
|
2015-06-06 05:15:14 -04:00
|
|
|
this._visit(ast.exp);
|
|
|
|
this._expression += ` | ${ast.name}`;
|
|
|
|
ast.args.forEach(arg => {
|
|
|
|
this._expression += ':';
|
|
|
|
this._visit(arg);
|
2015-06-04 13:06:09 -04:00
|
|
|
});
|
|
|
|
this._expression += ')';
|
2015-06-06 05:15:14 -04:00
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitFunctionCall(ast: FunctionCall, context: any) {
|
2020-04-08 13:14:18 -04:00
|
|
|
this._visit(ast.target!);
|
2015-06-06 05:15:14 -04:00
|
|
|
this._expression += '(';
|
2016-11-12 08:08:58 -05:00
|
|
|
let isFirst = true;
|
2015-06-06 05:15:14 -04:00
|
|
|
ast.args.forEach(arg => {
|
|
|
|
if (!isFirst) this._expression += ', ';
|
|
|
|
isFirst = false;
|
|
|
|
this._visit(arg);
|
|
|
|
});
|
|
|
|
this._expression += ')';
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}
|
2015-06-06 05:15:14 -04:00
|
|
|
|
fix(compiler): preserve this.$event and this.$any accesses in expressions (#39323)
Currently expressions `$event.foo()` and `this.$event.foo()`, as well as `$any(foo)` and
`this.$any(foo)`, are treated as the same expression by the compiler, because `this` is considered
the same implicit receiver as when the receiver is omitted. This introduces the following issues:
1. Any time something called `$any` is used, it'll be stripped away, leaving only the first parameter.
2. If something called `$event` is used anywhere in a template, it'll be preserved as `$event`,
rather than being rewritten to `ctx.$event`, causing the value to undefined at runtime. This
applies to listener, property and text bindings.
These changes resolve the first issue and part of the second one by preserving anything that
is accessed through `this`, even if it's one of the "special" ones like `$any` or `$event`.
Furthermore, these changes only expose the `$event` global variable inside event listeners,
whereas previously it was available everywhere.
Fixes #30278.
PR Close #39323
2020-10-18 11:41:29 -04:00
|
|
|
visitThisReceiver(ast: ThisReceiver, context: any) {}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitInterpolation(ast: Interpolation, context: any) {
|
2015-06-06 05:15:14 -04:00
|
|
|
for (let i = 0; i < ast.strings.length; i++) {
|
|
|
|
this._expression += ast.strings[i];
|
|
|
|
if (i < ast.expressions.length) {
|
2016-06-20 12:52:41 -04:00
|
|
|
this._expression += `${this._interpolationConfig.start} `;
|
2015-06-06 05:15:14 -04:00
|
|
|
this._visit(ast.expressions[i]);
|
2016-06-20 12:52:41 -04:00
|
|
|
this._expression += ` ${this._interpolationConfig.end}`;
|
2015-06-06 05:15:14 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitKeyedRead(ast: KeyedRead, context: any) {
|
2015-06-06 05:15:14 -04:00
|
|
|
this._visit(ast.obj);
|
|
|
|
this._expression += '[';
|
|
|
|
this._visit(ast.key);
|
|
|
|
this._expression += ']';
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitKeyedWrite(ast: KeyedWrite, context: any) {
|
2015-08-12 19:26:21 -04:00
|
|
|
this._visit(ast.obj);
|
|
|
|
this._expression += '[';
|
|
|
|
this._visit(ast.key);
|
|
|
|
this._expression += '] = ';
|
|
|
|
this._visit(ast.value);
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitLiteralArray(ast: LiteralArray, context: any) {
|
2015-06-06 05:15:14 -04:00
|
|
|
this._expression += '[';
|
2016-11-12 08:08:58 -05:00
|
|
|
let isFirst = true;
|
2015-06-06 05:15:14 -04:00
|
|
|
ast.expressions.forEach(expression => {
|
|
|
|
if (!isFirst) this._expression += ', ';
|
|
|
|
isFirst = false;
|
|
|
|
this._visit(expression);
|
|
|
|
});
|
|
|
|
|
|
|
|
this._expression += ']';
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitLiteralMap(ast: LiteralMap, context: any) {
|
2015-06-06 05:15:14 -04:00
|
|
|
this._expression += '{';
|
2016-11-12 08:08:58 -05:00
|
|
|
let isFirst = true;
|
2015-06-06 05:15:14 -04:00
|
|
|
for (let i = 0; i < ast.keys.length; i++) {
|
|
|
|
if (!isFirst) this._expression += ', ';
|
|
|
|
isFirst = false;
|
2017-07-05 13:54:05 -04:00
|
|
|
const key = ast.keys[i];
|
|
|
|
this._expression += key.quoted ? JSON.stringify(key.key) : key.key;
|
|
|
|
this._expression += ': ';
|
2015-06-06 05:15:14 -04:00
|
|
|
this._visit(ast.values[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
this._expression += '}';
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitLiteralPrimitive(ast: LiteralPrimitive, context: any) {
|
2016-10-19 16:42:39 -04:00
|
|
|
if (typeof ast.value === 'string') {
|
2020-04-08 13:14:18 -04:00
|
|
|
this._expression += `"${ast.value.replace(Unparser._quoteRegExp, '\"')}"`;
|
2015-06-06 05:15:14 -04:00
|
|
|
} else {
|
|
|
|
this._expression += `${ast.value}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitMethodCall(ast: MethodCall, context: any) {
|
2015-06-06 05:15:14 -04:00
|
|
|
this._visit(ast.receiver);
|
2015-07-10 05:29:41 -04:00
|
|
|
this._expression += ast.receiver instanceof ImplicitReceiver ? `${ast.name}(` : `.${ast.name}(`;
|
2016-11-12 08:08:58 -05:00
|
|
|
let isFirst = true;
|
2015-06-06 05:15:14 -04:00
|
|
|
ast.args.forEach(arg => {
|
|
|
|
if (!isFirst) this._expression += ', ';
|
|
|
|
isFirst = false;
|
|
|
|
this._visit(arg);
|
|
|
|
});
|
|
|
|
this._expression += ')';
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitPrefixNot(ast: PrefixNot, context: any) {
|
2015-06-06 05:15:14 -04:00
|
|
|
this._expression += '!';
|
|
|
|
this._visit(ast.expression);
|
|
|
|
}
|
|
|
|
|
2017-05-11 13:15:54 -04:00
|
|
|
visitNonNullAssert(ast: NonNullAssert, context: any) {
|
|
|
|
this._visit(ast.expression);
|
|
|
|
this._expression += '!';
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitSafePropertyRead(ast: SafePropertyRead, context: any) {
|
2015-06-06 05:15:14 -04:00
|
|
|
this._visit(ast.receiver);
|
|
|
|
this._expression += `?.${ast.name}`;
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitSafeMethodCall(ast: SafeMethodCall, context: any) {
|
2015-06-06 05:15:14 -04:00
|
|
|
this._visit(ast.receiver);
|
|
|
|
this._expression += `?.${ast.name}(`;
|
2016-11-12 08:08:58 -05:00
|
|
|
let isFirst = true;
|
2015-06-06 05:15:14 -04:00
|
|
|
ast.args.forEach(arg => {
|
|
|
|
if (!isFirst) this._expression += ', ';
|
|
|
|
isFirst = false;
|
|
|
|
this._visit(arg);
|
|
|
|
});
|
|
|
|
this._expression += ')';
|
|
|
|
}
|
|
|
|
|
2016-01-06 17:13:44 -05:00
|
|
|
visitQuote(ast: Quote, context: any) {
|
|
|
|
this._expression += `${ast.prefix}:${ast.uninterpretedExpression}`;
|
|
|
|
}
|
2015-11-23 20:58:12 -05:00
|
|
|
|
2020-04-08 13:14:18 -04:00
|
|
|
private _visit(ast: AST) {
|
|
|
|
ast.visit(this);
|
|
|
|
}
|
2015-06-06 05:15:14 -04:00
|
|
|
}
|
2016-07-06 17:06:47 -04:00
|
|
|
|
|
|
|
const sharedUnparser = new Unparser();
|
|
|
|
|
|
|
|
export function unparse(
|
|
|
|
ast: AST, interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): string {
|
|
|
|
return sharedUnparser.unparse(ast, interpolationConfig);
|
|
|
|
}
|
2020-04-27 21:54:30 -04:00
|
|
|
|
|
|
|
// [unparsed AST, original source code of AST]
|
|
|
|
type UnparsedWithSpan = [string, string];
|
|
|
|
|
|
|
|
export function unparseWithSpan(
|
|
|
|
ast: ASTWithSource,
|
|
|
|
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): UnparsedWithSpan[] {
|
|
|
|
const unparsed: UnparsedWithSpan[] = [];
|
|
|
|
const source = ast.source!;
|
|
|
|
const recursiveSpanUnparser = new class extends RecursiveAstVisitor {
|
|
|
|
private recordUnparsed(ast: any, spanKey: string, unparsedList: UnparsedWithSpan[]) {
|
|
|
|
const span = ast[spanKey];
|
|
|
|
const prefix = spanKey === 'span' ? '' : `[${spanKey}] `;
|
|
|
|
const src = source.substring(span.start, span.end);
|
|
|
|
unparsedList.push([
|
|
|
|
unparse(ast, interpolationConfig),
|
|
|
|
prefix + src,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
visit(ast: AST, unparsedList: UnparsedWithSpan[]) {
|
|
|
|
this.recordUnparsed(ast, 'span', unparsedList);
|
|
|
|
if (ast.hasOwnProperty('nameSpan')) {
|
|
|
|
this.recordUnparsed(ast, 'nameSpan', unparsedList);
|
|
|
|
}
|
|
|
|
ast.visit(this, unparsedList);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
recursiveSpanUnparser.visitAll([ast.ast], unparsed);
|
|
|
|
return unparsed;
|
|
|
|
}
|