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
This commit is contained in:
Kristiyan Kostadinov 2020-10-18 17:41:29 +02:00 committed by Joey Perrott
parent 4e68254514
commit cbc0907bfd
16 changed files with 348 additions and 30 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, Unary} from '@angular/compiler'; import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, ThisReceiver, Unary} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {TypeCheckingConfig} from '../api'; import {TypeCheckingConfig} from '../api';
@ -137,6 +137,10 @@ class AstTranslator implements AstVisitor {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
visitThisReceiver(ast: ThisReceiver): never {
throw new Error('Method not implemented.');
}
visitInterpolation(ast: Interpolation): ts.Expression { visitInterpolation(ast: Interpolation): ts.Expression {
// Build up a chain of binary + operations to simulate the string concatenation of the // Build up a chain of binary + operations to simulate the string concatenation of the
// interpolation's expressions. The chain is started using an actual string literal to ensure // interpolation's expressions. The chain is started using an actual string literal to ensure
@ -363,6 +367,9 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor {
visitImplicitReceiver(ast: ImplicitReceiver): boolean { visitImplicitReceiver(ast: ImplicitReceiver): boolean {
return false; return false;
} }
visitThisReceiver(ast: ThisReceiver): boolean {
return false;
}
visitInterpolation(ast: Interpolation): boolean { visitInterpolation(ast: Interpolation): boolean {
return ast.expressions.some(exp => exp.visit(this)); return ast.expressions.some(exp => exp.visit(this));
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AST, BindingPipe, BindingType, BoundTarget, DYNAMIC_TYPE, ImplicitReceiver, MethodCall, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, SchemaMetadata, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstElement, TmplAstIcu, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler'; import {AST, BindingPipe, BindingType, BoundTarget, DYNAMIC_TYPE, ImplicitReceiver, MethodCall, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, SchemaMetadata, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstElement, TmplAstIcu, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Reference} from '../../imports'; import {Reference} from '../../imports';
@ -1586,7 +1586,9 @@ class TcbExpressionTranslator {
const result = tsCallMethod(pipe, 'transform', [expr, ...args]); const result = tsCallMethod(pipe, 'transform', [expr, ...args]);
addParseSpanInfo(result, ast.sourceSpan); addParseSpanInfo(result, ast.sourceSpan);
return result; return result;
} else if (ast instanceof MethodCall && ast.receiver instanceof ImplicitReceiver) { } else if (
ast instanceof MethodCall && ast.receiver instanceof ImplicitReceiver &&
!(ast.receiver instanceof ThisReceiver)) {
// Resolve the special `$any(expr)` syntax to insert a cast of the argument to type `any`. // Resolve the special `$any(expr)` syntax to insert a cast of the argument to type `any`.
// `$any(expr)` -> `expr as any` // `$any(expr)` -> `expr as any`
if (ast.name === '$any' && ast.args.length === 1) { if (ast.name === '$any' && ast.args.length === 1) {
@ -1843,7 +1845,7 @@ class TcbEventHandlerTranslator extends TcbExpressionTranslator {
// function that the converted expression becomes a child of, just create a reference to the // function that the converted expression becomes a child of, just create a reference to the
// parameter by its name. // parameter by its name.
if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver && if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver &&
ast.name === EVENT_PARAMETER) { !(ast.receiver instanceof ThisReceiver) && ast.name === EVENT_PARAMETER) {
const event = ts.createIdentifier(EVENT_PARAMETER); const event = ts.createIdentifier(EVENT_PARAMETER);
addParseSpanInfo(event, ast.nameSpan); addParseSpanInfo(event, ast.nameSpan);
return event; return event;

View File

@ -584,6 +584,12 @@ describe('type check blocks', () => {
expect(block).toContain('(((ctx).a) as any)'); expect(block).toContain('(((ctx).a) as any)');
}); });
it('should handle $any accessed through `this`', () => {
const TEMPLATE = `{{this.$any(a)}}`;
const block = tcb(TEMPLATE);
expect(block).toContain('((ctx).$any(((ctx).a)))');
});
describe('experimental DOM checking via lib.dom.d.ts', () => { describe('experimental DOM checking via lib.dom.d.ts', () => {
it('should translate unclaimed bindings to their property equivalent', () => { it('should translate unclaimed bindings to their property equivalent', () => {
const TEMPLATE = `<label [for]="'test'"></label>`; const TEMPLATE = `<label [for]="'test'"></label>`;
@ -684,6 +690,14 @@ describe('type check blocks', () => {
expect(block).toContain( expect(block).toContain(
'_t3.addEventListener("event", function ($event): any { (_t2 = 3); });'); '_t3.addEventListener("event", function ($event): any { (_t2 = 3); });');
}); });
it('should ignore accesses to $event through `this`', () => {
const TEMPLATE = `<div (event)="foo(this.$event)"></div>`;
const block = tcb(TEMPLATE);
expect(block).toContain(
'_t1.addEventListener("event", function ($event): any { (ctx).foo(((ctx).$event)); });');
});
}); });
describe('config', () => { describe('config', () => {

View File

@ -447,4 +447,137 @@ describe('compiler compliance: listen()', () => {
const result = compile(files, angularFiles); const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect host bindings'); expectEmit(result.source, template, 'Incorrect host bindings');
}); });
it('should assume $event is referring to the event variable in a listener by default', () => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: '<div (click)="c($event)"></div>'
})
class Comp {
c(event: MouseEvent) {}
}
`
}
};
const template = `
i0.ɵɵlistener("click", function Comp_Template_div_click_0_listener($event) { return ctx.c($event); });
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect event listener');
});
it('should preserve accesses to $event if it is done through `this` in a listener', () => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: '<div (click)="c(this.$event)"></div>'
})
class Comp {
$event = {};
c(value: {}) {}
}
`
}
};
const template = `
i0.ɵɵlistener("click", function Comp_Template_div_click_0_listener() { return ctx.c(ctx.$event); });
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect event listener');
});
it('should not assume that $event is referring to an event object inside a property', () => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: '<div [event]="$event"></div>'
})
class Comp {
$event = 1;
}
`
}
};
const template = `
i0.ɵɵproperty("event", ctx.$event);
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect property binding');
});
it('should assume $event is referring to the event variable in a listener by default inside a host binding',
() => {
const files = {
app: {
'spec.ts': `
import {Directive} from '@angular/core';
@Directive({
host: {
'(click)': 'c($event)'
}
})
class Dir {
c(event: MouseEvent) {}
}
`
}
};
const template = `
i0.ɵɵlistener("click", function Dir_click_HostBindingHandler($event) { return ctx.c($event); });
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect event listener');
});
it('should preserve accesses to $event if it is done through `this` in a listener inside a host binding',
() => {
const files = {
app: {
'spec.ts': `
import {Directive} from '@angular/core';
@Directive({
host: {
'(click)': 'c(this.$event)'
}
})
class Dir {
$event = {};
c(value: {}) {}
}
`
}
};
const template = `
i0.ɵɵlistener("click", function Dir_click_HostBindingHandler() { return ctx.c(ctx.$event); });
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect event listener');
});
}); });

View File

@ -191,4 +191,57 @@ describe('r3_view_compiler', () => {
expectEmit(result.source, template, 'Incorrect initialization attributes'); expectEmit(result.source, template, 'Incorrect initialization attributes');
}); });
}); });
describe('$any', () => {
it('should strip out $any wrappers', () => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: '<div [tabIndex]="$any(10)"></div>'
})
class Comp {
}
`
}
};
const template = `
i0.ɵɵproperty("tabIndex", 10);
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should preserve $any if it is accessed through `this`', () => {
const files = {
app: {
'spec.ts': `
import {Component} from '@angular/core';
@Component({
template: '<div [tabIndex]="this.$any(null)"></div>'
})
class Comp {
$any(value: null): any {
return value as any;
}
}
`
}
};
const template = `
i0.ɵɵproperty("tabIndex", ctx.$any(null));
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
});
}); });

View File

@ -18,6 +18,7 @@ export class EventHandlerVars {
export interface LocalResolver { export interface LocalResolver {
getLocal(name: string): o.Expression|null; getLocal(name: string): o.Expression|null;
notifyImplicitReceiverUse(): void; notifyImplicitReceiverUse(): void;
globals?: Set<string>;
} }
export class ConvertActionBindingResult { export class ConvertActionBindingResult {
@ -72,10 +73,10 @@ export type InterpolationFunction = (args: o.Expression[]) => o.Expression;
export function convertActionBinding( export function convertActionBinding(
localResolver: LocalResolver|null, implicitReceiver: o.Expression, action: cdAst.AST, localResolver: LocalResolver|null, implicitReceiver: o.Expression, action: cdAst.AST,
bindingId: string, interpolationFunction?: InterpolationFunction, bindingId: string, interpolationFunction?: InterpolationFunction,
baseSourceSpan?: ParseSourceSpan, baseSourceSpan?: ParseSourceSpan, implicitReceiverAccesses?: Set<string>,
implicitReceiverAccesses?: Set<string>): ConvertActionBindingResult { globals?: Set<string>): ConvertActionBindingResult {
if (!localResolver) { if (!localResolver) {
localResolver = new DefaultLocalResolver(); localResolver = new DefaultLocalResolver(globals);
} }
const actionWithoutBuiltins = convertPropertyBindingBuiltins( const actionWithoutBuiltins = convertPropertyBindingBuiltins(
{ {
@ -446,6 +447,10 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
return this._implicitReceiver; return this._implicitReceiver;
} }
visitThisReceiver(ast: cdAst.ThisReceiver, mode: _Mode): any {
return this.visitImplicitReceiver(ast, mode);
}
visitInterpolation(ast: cdAst.Interpolation, mode: _Mode): any { visitInterpolation(ast: cdAst.Interpolation, mode: _Mode): any {
ensureExpressionMode(mode, ast); ensureExpressionMode(mode, ast);
const args = [o.literal(ast.expressions.length)]; const args = [o.literal(ast.expressions.length)];
@ -501,12 +506,17 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
mode, o.literal(ast.value, type, this.convertSourceSpan(ast.span))); mode, o.literal(ast.value, type, this.convertSourceSpan(ast.span)));
} }
private _getLocal(name: string): o.Expression|null { private _getLocal(name: string, receiver: cdAst.AST): o.Expression|null {
if (this._localResolver.globals?.has(name) && receiver instanceof cdAst.ThisReceiver) {
return null;
}
return this._localResolver.getLocal(name); return this._localResolver.getLocal(name);
} }
visitMethodCall(ast: cdAst.MethodCall, mode: _Mode): any { visitMethodCall(ast: cdAst.MethodCall, mode: _Mode): any {
if (ast.receiver instanceof cdAst.ImplicitReceiver && ast.name == '$any') { if (ast.receiver instanceof cdAst.ImplicitReceiver &&
!(ast.receiver instanceof cdAst.ThisReceiver) && ast.name === '$any') {
const args = this.visitAll(ast.args, _Mode.Expression) as any[]; const args = this.visitAll(ast.args, _Mode.Expression) as any[];
if (args.length != 1) { if (args.length != 1) {
throw new Error( throw new Error(
@ -524,14 +534,14 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
let result: any = null; let result: any = null;
const receiver = this._visit(ast.receiver, _Mode.Expression); const receiver = this._visit(ast.receiver, _Mode.Expression);
if (receiver === this._implicitReceiver) { if (receiver === this._implicitReceiver) {
const varExpr = this._getLocal(ast.name); const varExpr = this._getLocal(ast.name, ast.receiver);
if (varExpr) { if (varExpr) {
// Restore the previous "usesImplicitReceiver" state since the implicit // Restore the previous "usesImplicitReceiver" state since the implicit
// receiver has been replaced with a resolved local expression. // receiver has been replaced with a resolved local expression.
this.usesImplicitReceiver = prevUsesImplicitReceiver; this.usesImplicitReceiver = prevUsesImplicitReceiver;
result = varExpr.callFn(args); result = varExpr.callFn(args);
this.addImplicitReceiverAccess(ast.name);
} }
this.addImplicitReceiverAccess(ast.name);
} }
if (result == null) { if (result == null) {
result = receiver.callMethod(ast.name, args, this.convertSourceSpan(ast.span)); result = receiver.callMethod(ast.name, args, this.convertSourceSpan(ast.span));
@ -558,13 +568,13 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
const prevUsesImplicitReceiver = this.usesImplicitReceiver; const prevUsesImplicitReceiver = this.usesImplicitReceiver;
const receiver = this._visit(ast.receiver, _Mode.Expression); const receiver = this._visit(ast.receiver, _Mode.Expression);
if (receiver === this._implicitReceiver) { if (receiver === this._implicitReceiver) {
result = this._getLocal(ast.name); result = this._getLocal(ast.name, ast.receiver);
if (result) { if (result) {
// Restore the previous "usesImplicitReceiver" state since the implicit // Restore the previous "usesImplicitReceiver" state since the implicit
// receiver has been replaced with a resolved local expression. // receiver has been replaced with a resolved local expression.
this.usesImplicitReceiver = prevUsesImplicitReceiver; this.usesImplicitReceiver = prevUsesImplicitReceiver;
this.addImplicitReceiverAccess(ast.name);
} }
this.addImplicitReceiverAccess(ast.name);
} }
if (result == null) { if (result == null) {
result = receiver.prop(ast.name); result = receiver.prop(ast.name);
@ -579,7 +589,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
let varExpr: o.ReadPropExpr|null = null; let varExpr: o.ReadPropExpr|null = null;
if (receiver === this._implicitReceiver) { if (receiver === this._implicitReceiver) {
const localExpr = this._getLocal(ast.name); const localExpr = this._getLocal(ast.name, ast.receiver);
if (localExpr) { if (localExpr) {
if (localExpr instanceof o.ReadPropExpr) { if (localExpr instanceof o.ReadPropExpr) {
// If the local variable is a property read expression, it's a reference // If the local variable is a property read expression, it's a reference
@ -748,6 +758,9 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
visitImplicitReceiver(ast: cdAst.ImplicitReceiver) { visitImplicitReceiver(ast: cdAst.ImplicitReceiver) {
return null; return null;
}, },
visitThisReceiver(ast: cdAst.ThisReceiver) {
return null;
},
visitInterpolation(ast: cdAst.Interpolation) { visitInterpolation(ast: cdAst.Interpolation) {
return null; return null;
}, },
@ -825,6 +838,9 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
visitImplicitReceiver(ast: cdAst.ImplicitReceiver) { visitImplicitReceiver(ast: cdAst.ImplicitReceiver) {
return false; return false;
}, },
visitThisReceiver(ast: cdAst.ThisReceiver) {
return false;
},
visitInterpolation(ast: cdAst.Interpolation) { visitInterpolation(ast: cdAst.Interpolation) {
return visitSome(this, ast.expressions); return visitSome(this, ast.expressions);
}, },
@ -924,6 +940,7 @@ function flattenStatements(arg: any, output: o.Statement[]) {
} }
class DefaultLocalResolver implements LocalResolver { class DefaultLocalResolver implements LocalResolver {
constructor(public globals?: Set<string>) {}
notifyImplicitReceiverUse(): void {} notifyImplicitReceiverUse(): void {}
getLocal(name: string): o.Expression|null { getLocal(name: string): o.Expression|null {
if (name === EventHandlerVars.event.name) { if (name === EventHandlerVars.event.name) {

View File

@ -85,6 +85,20 @@ export class ImplicitReceiver extends AST {
} }
} }
/**
* Receiver when something is accessed through `this` (e.g. `this.foo`). Note that this class
* inherits from `ImplicitReceiver`, because accessing something through `this` is treated the
* same as accessing it implicitly inside of an Angular template (e.g. `[attr.title]="this.title"`
* is the same as `[attr.title]="title"`.). Inheriting allows for the `this` accesses to be treated
* the same as implicit ones, except for a couple of exceptions like `$event` and `$any`.
* TODO: we should find a way for this class not to extend from `ImplicitReceiver` in the future.
*/
export class ThisReceiver extends ImplicitReceiver {
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitThisReceiver?.(this, context);
}
}
/** /**
* Multiple expressions separated by a semicolon. * Multiple expressions separated by a semicolon.
*/ */
@ -416,6 +430,11 @@ export interface AstVisitor {
visitChain(ast: Chain, context: any): any; visitChain(ast: Chain, context: any): any;
visitConditional(ast: Conditional, context: any): any; visitConditional(ast: Conditional, context: any): any;
visitFunctionCall(ast: FunctionCall, context: any): any; visitFunctionCall(ast: FunctionCall, context: any): any;
/**
* The `visitThisReceiver` method is declared as optional for backwards compatibility.
* In an upcoming major release, this method will be made required.
*/
visitThisReceiver?(ast: ThisReceiver, context: any): any;
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any; visitImplicitReceiver(ast: ImplicitReceiver, context: any): any;
visitInterpolation(ast: Interpolation, context: any): any; visitInterpolation(ast: Interpolation, context: any): any;
visitKeyedRead(ast: KeyedRead, context: any): any; visitKeyedRead(ast: KeyedRead, context: any): any;
@ -474,7 +493,8 @@ export class RecursiveAstVisitor implements AstVisitor {
} }
this.visitAll(ast.args, context); this.visitAll(ast.args, context);
} }
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any {} visitImplicitReceiver(ast: ThisReceiver, context: any): any {}
visitThisReceiver(ast: ThisReceiver, context: any): any {}
visitInterpolation(ast: Interpolation, context: any): any { visitInterpolation(ast: Interpolation, context: any): any {
this.visitAll(ast.expressions, context); this.visitAll(ast.expressions, context);
} }
@ -532,6 +552,10 @@ export class AstTransformer implements AstVisitor {
return ast; return ast;
} }
visitThisReceiver(ast: ThisReceiver, context: any): AST {
return ast;
}
visitInterpolation(ast: Interpolation, context: any): AST { visitInterpolation(ast: Interpolation, context: any): AST {
return new Interpolation(ast.span, ast.sourceSpan, ast.strings, this.visitAll(ast.expressions)); return new Interpolation(ast.span, ast.sourceSpan, ast.strings, this.visitAll(ast.expressions));
} }
@ -651,6 +675,10 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
return ast; return ast;
} }
visitThisReceiver(ast: ThisReceiver, context: any): AST {
return ast;
}
visitInterpolation(ast: Interpolation, context: any): Interpolation { visitInterpolation(ast: Interpolation, context: any): Interpolation {
const expressions = this.visitAll(ast.expressions); const expressions = this.visitAll(ast.expressions);
if (expressions !== ast.expressions) if (expressions !== ast.expressions)

View File

@ -10,7 +10,7 @@ import * as chars from '../chars';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
import {escapeRegExp} from '../util'; import {escapeRegExp} from '../util';
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, Unary, VariableBinding} from './ast'; import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, ThisReceiver, Unary, VariableBinding} from './ast';
import {EOF, isIdentifier, isQuote, Lexer, Token, TokenType} from './lexer'; import {EOF, isIdentifier, isQuote, Lexer, Token, TokenType} from './lexer';
export class SplitInterpolation { export class SplitInterpolation {
@ -784,8 +784,7 @@ export class _ParseAST {
} else if (this.next.isKeywordThis()) { } else if (this.next.isKeywordThis()) {
this.advance(); this.advance();
return new ImplicitReceiver(this.span(start), this.sourceSpan(start)); return new ThisReceiver(this.span(start), this.sourceSpan(start));
} else if (this.consumeOptionalCharacter(chars.$LBRACKET)) { } else if (this.consumeOptionalCharacter(chars.$LBRACKET)) {
this.rbracketsExpected++; this.rbracketsExpected++;
const elements = this.parseExpressionList(chars.$RBRACKET); const elements = this.parseExpressionList(chars.$RBRACKET);
@ -1158,6 +1157,8 @@ class SimpleExpressionChecker implements AstVisitor {
visitImplicitReceiver(ast: ImplicitReceiver, context: any) {} visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}
visitThisReceiver(ast: ThisReceiver, context: any) {}
visitInterpolation(ast: Interpolation, context: any) {} visitInterpolation(ast: Interpolation, context: any) {}
visitLiteralPrimitive(ast: LiteralPrimitive, context: any) {} visitLiteralPrimitive(ast: LiteralPrimitive, context: any) {}

View File

@ -10,7 +10,7 @@ import {flatten, sanitizeIdentifier} from '../../compile_metadata';
import {BindingForm, BuiltinFunctionCall, convertActionBinding, convertPropertyBinding, convertUpdateArguments, LocalResolver} from '../../compiler_util/expression_converter'; import {BindingForm, BuiltinFunctionCall, convertActionBinding, convertPropertyBinding, convertUpdateArguments, LocalResolver} from '../../compiler_util/expression_converter';
import {ConstantPool} from '../../constant_pool'; import {ConstantPool} from '../../constant_pool';
import * as core from '../../core'; import * as core from '../../core';
import {AST, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, Interpolation, LiteralArray, LiteralMap, LiteralPrimitive, ParsedEventType, PropertyRead} from '../../expression_parser/ast'; import {AST, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, Interpolation, LiteralArray, LiteralMap, LiteralPrimitive, ParsedEventType, PropertyRead, ThisReceiver} from '../../expression_parser/ast';
import {Lexer} from '../../expression_parser/lexer'; import {Lexer} from '../../expression_parser/lexer';
import {IvyParser} from '../../expression_parser/parser'; import {IvyParser} from '../../expression_parser/parser';
import * as i18n from '../../i18n/i18n_ast'; import * as i18n from '../../i18n/i18n_ast';
@ -48,6 +48,9 @@ const NG_CONTENT_SELECT_ATTR = 'select';
// Attribute name of `ngProjectAs`. // Attribute name of `ngProjectAs`.
const NG_PROJECT_AS_ATTR_NAME = 'ngProjectAs'; const NG_PROJECT_AS_ATTR_NAME = 'ngProjectAs';
// Global symbols available only inside event bindings.
const EVENT_BINDING_SCOPE_GLOBALS = new Set<string>(['$event']);
// List of supported global targets for event listeners // List of supported global targets for event listeners
const GLOBAL_TARGET_RESOLVERS = new Map<string, o.ExternalReference>( const GLOBAL_TARGET_RESOLVERS = new Map<string, o.ExternalReference>(
[['window', R3.resolveWindow], ['document', R3.resolveDocument], ['body', R3.resolveBody]]); [['window', R3.resolveWindow], ['document', R3.resolveDocument], ['body', R3.resolveBody]]);
@ -76,7 +79,7 @@ export function prepareEventListenerParameters(
scope.getOrCreateSharedContextVar(0); scope.getOrCreateSharedContextVar(0);
const bindingExpr = convertActionBinding( const bindingExpr = convertActionBinding(
scope, implicitReceiverExpr, handler, 'b', () => error('Unexpected interpolation'), scope, implicitReceiverExpr, handler, 'b', () => error('Unexpected interpolation'),
eventAst.handlerSpan, implicitReceiverAccesses); eventAst.handlerSpan, implicitReceiverAccesses, EVENT_BINDING_SCOPE_GLOBALS);
const statements = []; const statements = [];
if (scope) { if (scope) {
statements.push(...scope.restoreViewStatement()); statements.push(...scope.restoreViewStatement());
@ -1427,7 +1430,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
prepareSyntheticListenerFunctionName(eventName, outputAst.phase!) : prepareSyntheticListenerFunctionName(eventName, outputAst.phase!) :
sanitizeIdentifier(eventName); sanitizeIdentifier(eventName);
const handlerName = `${this.templateName}_${tagName}_${bindingFnName}_${index}_listener`; const handlerName = `${this.templateName}_${tagName}_${bindingFnName}_${index}_listener`;
const scope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel); const scope = this._bindingScope.nestedScope(
this._bindingScope.bindingLevel, EVENT_BINDING_SCOPE_GLOBALS);
return prepareEventListenerParameters(outputAst, handlerName, scope); return prepareEventListenerParameters(outputAst, handlerName, scope);
}; };
} }
@ -1625,10 +1629,18 @@ export class BindingScope implements LocalResolver {
private referenceNameIndex = 0; private referenceNameIndex = 0;
private restoreViewVariable: o.ReadVarExpr|null = null; private restoreViewVariable: o.ReadVarExpr|null = null;
static createRootScope(): BindingScope { static createRootScope(): BindingScope {
return new BindingScope().set(0, '$event', o.variable('$event')); return new BindingScope();
} }
private constructor(public bindingLevel: number = 0, private parent: BindingScope|null = null) {} private constructor(
public bindingLevel: number = 0, private parent: BindingScope|null = null,
public globals?: Set<string>) {
if (globals !== undefined) {
for (const name of globals) {
this.set(0, name, o.variable(name));
}
}
}
get(name: string): o.Expression|null { get(name: string): o.Expression|null {
let current: BindingScope|null = this; let current: BindingScope|null = this;
@ -1715,8 +1727,8 @@ export class BindingScope implements LocalResolver {
} }
} }
nestedScope(level: number): BindingScope { nestedScope(level: number, globals?: Set<string>): BindingScope {
const newScope = new BindingScope(level, this); const newScope = new BindingScope(level, this, globals);
if (level > 0) newScope.generateSharedContextVar(0); if (level > 0) newScope.generateSharedContextVar(0);
return newScope; return newScope;
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
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, Unary} from '../../../src/expression_parser/ast'; 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';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../src/ml_parser/interpolation_config'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../src/ml_parser/interpolation_config';
class Unparser implements AstVisitor { class Unparser implements AstVisitor {
@ -87,6 +87,8 @@ class Unparser implements AstVisitor {
visitImplicitReceiver(ast: ImplicitReceiver, context: any) {} visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}
visitThisReceiver(ast: ThisReceiver, context: any) {}
visitInterpolation(ast: Interpolation, context: any) { visitInterpolation(ast: Interpolation, context: any) {
for (let i = 0; i < ast.strings.length; i++) { for (let i = 0; i < ast.strings.length; i++) {
this._expression += ast.strings[i]; this._expression += ast.strings[i];

View File

@ -384,6 +384,20 @@ describe('acceptance integration tests', () => {
expect(fixture.nativeElement.innerHTML).toEqual(''); expect(fixture.nativeElement.innerHTML).toEqual('');
}); });
it('should be able to render the result of a function called $any by using this', () => {
@Component({template: '{{this.$any(1, 2)}}'})
class App {
$any(value: number, multiplier: number) {
return value * multiplier;
}
}
TestBed.configureTestingModule({declarations: [App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('2');
});
}); });
describe('ngNonBindable handling', () => { describe('ngNonBindable handling', () => {

View File

@ -396,4 +396,33 @@ describe('event listeners', () => {
button.click(); button.click();
expect(comp.counter).toBe(1); expect(comp.counter).toBe(1);
}); });
onlyInIvy('issue has only been resolved for Ivy')
.it('should be able to access a property called $event using `this`', () => {
let eventVariable: number|undefined;
let eventObject: MouseEvent|undefined;
@Component({
template: `
<button (click)="clicked(this.$event, $event)">Click me!</button>
`,
})
class MyComp {
$event = 10;
clicked(value: number, event: MouseEvent) {
eventVariable = value;
eventObject = event;
}
}
TestBed.configureTestingModule({declarations: [MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
fixture.nativeElement.querySelector('button').click();
fixture.detectChanges();
expect(eventVariable).toBe(10);
expect(eventObject?.type).toBe('click');
});
}); });

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {AST, BindingPipe, ImplicitReceiver, MethodCall, TmplAstBoundAttribute, TmplAstNode, TmplAstTextAttribute} from '@angular/compiler'; import {AST, BindingPipe, ImplicitReceiver, MethodCall, ThisReceiver, TmplAstBoundAttribute, TmplAstNode, TmplAstTextAttribute} from '@angular/compiler';
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import * as ts from 'typescript'; import * as ts from 'typescript';
@ -208,7 +208,7 @@ function displayPartsEqual(a: {text: string, kind: string}, b: {text: string, ki
function isDollarAny(node: TmplAstNode|AST): node is MethodCall { function isDollarAny(node: TmplAstNode|AST): node is MethodCall {
return node instanceof MethodCall && node.receiver instanceof ImplicitReceiver && return node instanceof MethodCall && node.receiver instanceof ImplicitReceiver &&
node.name === '$any' && node.args.length === 1; !(node.receiver instanceof ThisReceiver) && node.name === '$any' && node.args.length === 1;
} }
function createDollarAnyQuickInfo(node: MethodCall): ts.QuickInfo { function createDollarAnyQuickInfo(node: MethodCall): ts.QuickInfo {

View File

@ -279,7 +279,7 @@ export function filterAliasImports(displayParts: ts.SymbolDisplayPart[]): ts.Sym
export function isDollarEvent(n: t.Node|e.AST): n is e.PropertyRead { export function isDollarEvent(n: t.Node|e.AST): n is e.PropertyRead {
return n instanceof e.PropertyRead && n.name === '$event' && return n instanceof e.PropertyRead && n.name === '$event' &&
n.receiver instanceof e.ImplicitReceiver; n.receiver instanceof e.ImplicitReceiver && !(n.receiver instanceof e.ThisReceiver);
} }
/** /**

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AST, AstVisitor, ASTWithName, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, Unary} from '@angular/compiler'; import {AST, AstVisitor, ASTWithName, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, ThisReceiver, Unary} from '@angular/compiler';
import {createDiagnostic, Diagnostic} from './diagnostic_messages'; import {createDiagnostic, Diagnostic} from './diagnostic_messages';
import {BuiltinType, Signature, Symbol, SymbolQuery, SymbolTable} from './symbols'; import {BuiltinType, Signature, Symbol, SymbolQuery, SymbolTable} from './symbols';
@ -258,6 +258,10 @@ export class AstType implements AstVisitor {
}; };
} }
visitThisReceiver(_ast: ThisReceiver): Symbol {
return this.visitImplicitReceiver(_ast);
}
visitInterpolation(ast: Interpolation): Symbol { visitInterpolation(ast: Interpolation): Symbol {
// If we are producing diagnostics, visit the children. // If we are producing diagnostics, visit the children.
for (const expr of ast.expressions) { for (const expr of ast.expressions) {

View File

@ -69,6 +69,7 @@ export function getExpressionCompletions(
visitConditional(_ast) {}, visitConditional(_ast) {},
visitFunctionCall(_ast) {}, visitFunctionCall(_ast) {},
visitImplicitReceiver(_ast) {}, visitImplicitReceiver(_ast) {},
visitThisReceiver(_ast) {},
visitInterpolation(_ast) { visitInterpolation(_ast) {
result = undefined; result = undefined;
}, },
@ -164,6 +165,7 @@ export function getExpressionSymbol(
visitConditional(_ast) {}, visitConditional(_ast) {},
visitFunctionCall(_ast) {}, visitFunctionCall(_ast) {},
visitImplicitReceiver(_ast) {}, visitImplicitReceiver(_ast) {},
visitThisReceiver(_ast) {},
visitInterpolation(_ast) {}, visitInterpolation(_ast) {},
visitKeyedRead(_ast) {}, visitKeyedRead(_ast) {},
visitKeyedWrite(_ast) {}, visitKeyedWrite(_ast) {},