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
*/
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 {TypeCheckingConfig} from '../api';
@ -137,6 +137,10 @@ class AstTranslator implements AstVisitor {
throw new Error('Method not implemented.');
}
visitThisReceiver(ast: ThisReceiver): never {
throw new Error('Method not implemented.');
}
visitInterpolation(ast: Interpolation): ts.Expression {
// 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
@ -363,6 +367,9 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor {
visitImplicitReceiver(ast: ImplicitReceiver): boolean {
return false;
}
visitThisReceiver(ast: ThisReceiver): boolean {
return false;
}
visitInterpolation(ast: Interpolation): boolean {
return ast.expressions.some(exp => exp.visit(this));
}

View File

@ -6,7 +6,7 @@
* 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 {Reference} from '../../imports';
@ -1586,7 +1586,9 @@ class TcbExpressionTranslator {
const result = tsCallMethod(pipe, 'transform', [expr, ...args]);
addParseSpanInfo(result, ast.sourceSpan);
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`.
// `$any(expr)` -> `expr as any`
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
// parameter by its name.
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);
addParseSpanInfo(event, ast.nameSpan);
return event;

View File

@ -584,6 +584,12 @@ describe('type check blocks', () => {
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', () => {
it('should translate unclaimed bindings to their property equivalent', () => {
const TEMPLATE = `<label [for]="'test'"></label>`;
@ -684,6 +690,14 @@ describe('type check blocks', () => {
expect(block).toContain(
'_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', () => {

View File

@ -447,4 +447,137 @@ describe('compiler compliance: listen()', () => {
const result = compile(files, angularFiles);
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');
});
});
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 {
getLocal(name: string): o.Expression|null;
notifyImplicitReceiverUse(): void;
globals?: Set<string>;
}
export class ConvertActionBindingResult {
@ -72,10 +73,10 @@ export type InterpolationFunction = (args: o.Expression[]) => o.Expression;
export function convertActionBinding(
localResolver: LocalResolver|null, implicitReceiver: o.Expression, action: cdAst.AST,
bindingId: string, interpolationFunction?: InterpolationFunction,
baseSourceSpan?: ParseSourceSpan,
implicitReceiverAccesses?: Set<string>): ConvertActionBindingResult {
baseSourceSpan?: ParseSourceSpan, implicitReceiverAccesses?: Set<string>,
globals?: Set<string>): ConvertActionBindingResult {
if (!localResolver) {
localResolver = new DefaultLocalResolver();
localResolver = new DefaultLocalResolver(globals);
}
const actionWithoutBuiltins = convertPropertyBindingBuiltins(
{
@ -446,6 +447,10 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
return this._implicitReceiver;
}
visitThisReceiver(ast: cdAst.ThisReceiver, mode: _Mode): any {
return this.visitImplicitReceiver(ast, mode);
}
visitInterpolation(ast: cdAst.Interpolation, mode: _Mode): any {
ensureExpressionMode(mode, ast);
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)));
}
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);
}
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[];
if (args.length != 1) {
throw new Error(
@ -524,14 +534,14 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
let result: any = null;
const receiver = this._visit(ast.receiver, _Mode.Expression);
if (receiver === this._implicitReceiver) {
const varExpr = this._getLocal(ast.name);
const varExpr = this._getLocal(ast.name, ast.receiver);
if (varExpr) {
// Restore the previous "usesImplicitReceiver" state since the implicit
// receiver has been replaced with a resolved local expression.
this.usesImplicitReceiver = prevUsesImplicitReceiver;
result = varExpr.callFn(args);
this.addImplicitReceiverAccess(ast.name);
}
this.addImplicitReceiverAccess(ast.name);
}
if (result == null) {
result = receiver.callMethod(ast.name, args, this.convertSourceSpan(ast.span));
@ -558,13 +568,13 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
const prevUsesImplicitReceiver = this.usesImplicitReceiver;
const receiver = this._visit(ast.receiver, _Mode.Expression);
if (receiver === this._implicitReceiver) {
result = this._getLocal(ast.name);
result = this._getLocal(ast.name, ast.receiver);
if (result) {
// Restore the previous "usesImplicitReceiver" state since the implicit
// receiver has been replaced with a resolved local expression.
this.usesImplicitReceiver = prevUsesImplicitReceiver;
this.addImplicitReceiverAccess(ast.name);
}
this.addImplicitReceiverAccess(ast.name);
}
if (result == null) {
result = receiver.prop(ast.name);
@ -579,7 +589,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
let varExpr: o.ReadPropExpr|null = null;
if (receiver === this._implicitReceiver) {
const localExpr = this._getLocal(ast.name);
const localExpr = this._getLocal(ast.name, ast.receiver);
if (localExpr) {
if (localExpr instanceof o.ReadPropExpr) {
// 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) {
return null;
},
visitThisReceiver(ast: cdAst.ThisReceiver) {
return null;
},
visitInterpolation(ast: cdAst.Interpolation) {
return null;
},
@ -825,6 +838,9 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
visitImplicitReceiver(ast: cdAst.ImplicitReceiver) {
return false;
},
visitThisReceiver(ast: cdAst.ThisReceiver) {
return false;
},
visitInterpolation(ast: cdAst.Interpolation) {
return visitSome(this, ast.expressions);
},
@ -924,6 +940,7 @@ function flattenStatements(arg: any, output: o.Statement[]) {
}
class DefaultLocalResolver implements LocalResolver {
constructor(public globals?: Set<string>) {}
notifyImplicitReceiverUse(): void {}
getLocal(name: string): o.Expression|null {
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.
*/
@ -416,6 +430,11 @@ export interface AstVisitor {
visitChain(ast: Chain, context: any): any;
visitConditional(ast: Conditional, 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;
visitInterpolation(ast: Interpolation, context: any): any;
visitKeyedRead(ast: KeyedRead, context: any): any;
@ -474,7 +493,8 @@ export class RecursiveAstVisitor implements AstVisitor {
}
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 {
this.visitAll(ast.expressions, context);
}
@ -532,6 +552,10 @@ export class AstTransformer implements AstVisitor {
return ast;
}
visitThisReceiver(ast: ThisReceiver, context: any): AST {
return ast;
}
visitInterpolation(ast: Interpolation, context: any): AST {
return new Interpolation(ast.span, ast.sourceSpan, ast.strings, this.visitAll(ast.expressions));
}
@ -651,6 +675,10 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
return ast;
}
visitThisReceiver(ast: ThisReceiver, context: any): AST {
return ast;
}
visitInterpolation(ast: Interpolation, context: any): Interpolation {
const expressions = this.visitAll(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 {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';
export class SplitInterpolation {
@ -784,8 +784,7 @@ export class _ParseAST {
} else if (this.next.isKeywordThis()) {
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)) {
this.rbracketsExpected++;
const elements = this.parseExpressionList(chars.$RBRACKET);
@ -1158,6 +1157,8 @@ class SimpleExpressionChecker implements AstVisitor {
visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}
visitThisReceiver(ast: ThisReceiver, context: any) {}
visitInterpolation(ast: Interpolation, 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 {ConstantPool} from '../../constant_pool';
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 {IvyParser} from '../../expression_parser/parser';
import * as i18n from '../../i18n/i18n_ast';
@ -48,6 +48,9 @@ const NG_CONTENT_SELECT_ATTR = 'select';
// Attribute name of `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
const GLOBAL_TARGET_RESOLVERS = new Map<string, o.ExternalReference>(
[['window', R3.resolveWindow], ['document', R3.resolveDocument], ['body', R3.resolveBody]]);
@ -76,7 +79,7 @@ export function prepareEventListenerParameters(
scope.getOrCreateSharedContextVar(0);
const bindingExpr = convertActionBinding(
scope, implicitReceiverExpr, handler, 'b', () => error('Unexpected interpolation'),
eventAst.handlerSpan, implicitReceiverAccesses);
eventAst.handlerSpan, implicitReceiverAccesses, EVENT_BINDING_SCOPE_GLOBALS);
const statements = [];
if (scope) {
statements.push(...scope.restoreViewStatement());
@ -1427,7 +1430,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
prepareSyntheticListenerFunctionName(eventName, outputAst.phase!) :
sanitizeIdentifier(eventName);
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);
};
}
@ -1625,10 +1629,18 @@ export class BindingScope implements LocalResolver {
private referenceNameIndex = 0;
private restoreViewVariable: o.ReadVarExpr|null = null;
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 {
let current: BindingScope|null = this;
@ -1715,8 +1727,8 @@ export class BindingScope implements LocalResolver {
}
}
nestedScope(level: number): BindingScope {
const newScope = new BindingScope(level, this);
nestedScope(level: number, globals?: Set<string>): BindingScope {
const newScope = new BindingScope(level, this, globals);
if (level > 0) newScope.generateSharedContextVar(0);
return newScope;
}

View File

@ -6,7 +6,7 @@
* 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';
class Unparser implements AstVisitor {
@ -87,6 +87,8 @@ class Unparser implements AstVisitor {
visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}
visitThisReceiver(ast: ThisReceiver, context: any) {}
visitInterpolation(ast: Interpolation, context: any) {
for (let i = 0; i < ast.strings.length; i++) {
this._expression += ast.strings[i];

View File

@ -384,6 +384,20 @@ describe('acceptance integration tests', () => {
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', () => {

View File

@ -396,4 +396,33 @@ describe('event listeners', () => {
button.click();
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
* 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 {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
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 {
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 {

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 {
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
*/
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 {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 {
// If we are producing diagnostics, visit the children.
for (const expr of ast.expressions) {

View File

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