diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts index f462987f84..f4360a183c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts @@ -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)); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 4864dfaec2..73608a6d4e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -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; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 886a78f958..ed60b9d31f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -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 = ``; @@ -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 = `
`; + const block = tcb(TEMPLATE); + + expect(block).toContain( + '_t1.addEventListener("event", function ($event): any { (ctx).foo(((ctx).$event)); });'); + }); }); describe('config', () => { diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts index 01d8d7880b..0173f6f0cd 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts @@ -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: '
' + }) + 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: '
' + }) + 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: '
' + }) + 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'); + }); }); diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts index b2e19b02b4..bdfbd1762d 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts @@ -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: '
' + }) + 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: '
' + }) + 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'); + }); + }); }); diff --git a/packages/compiler/src/compiler_util/expression_converter.ts b/packages/compiler/src/compiler_util/expression_converter.ts index 1bae07ce32..58017f5c73 100644 --- a/packages/compiler/src/compiler_util/expression_converter.ts +++ b/packages/compiler/src/compiler_util/expression_converter.ts @@ -18,6 +18,7 @@ export class EventHandlerVars { export interface LocalResolver { getLocal(name: string): o.Expression|null; notifyImplicitReceiverUse(): void; + globals?: Set; } 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): ConvertActionBindingResult { + baseSourceSpan?: ParseSourceSpan, implicitReceiverAccesses?: Set, + globals?: Set): 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) {} notifyImplicitReceiverUse(): void {} getLocal(name: string): o.Expression|null { if (name === EventHandlerVars.event.name) { diff --git a/packages/compiler/src/expression_parser/ast.ts b/packages/compiler/src/expression_parser/ast.ts index 2773a5458d..08589c143c 100644 --- a/packages/compiler/src/expression_parser/ast.ts +++ b/packages/compiler/src/expression_parser/ast.ts @@ -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) diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index 7d1cc1f4a4..2f488d2931 100644 --- a/packages/compiler/src/expression_parser/parser.ts +++ b/packages/compiler/src/expression_parser/parser.ts @@ -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) {} diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 4fc8c24bb9..f6e8707313 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -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(['$event']); + // List of supported global targets for event listeners const GLOBAL_TARGET_RESOLVERS = new Map( [['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, 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) { + 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): BindingScope { + const newScope = new BindingScope(level, this, globals); if (level > 0) newScope.generateSharedContextVar(0); return newScope; } diff --git a/packages/compiler/test/expression_parser/utils/unparser.ts b/packages/compiler/test/expression_parser/utils/unparser.ts index c16dae0d51..cb165b246b 100644 --- a/packages/compiler/test/expression_parser/utils/unparser.ts +++ b/packages/compiler/test/expression_parser/utils/unparser.ts @@ -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]; diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts index 5d07cbcefa..21e0c0000a 100644 --- a/packages/core/test/acceptance/integration_spec.ts +++ b/packages/core/test/acceptance/integration_spec.ts @@ -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', () => { diff --git a/packages/core/test/acceptance/listener_spec.ts b/packages/core/test/acceptance/listener_spec.ts index 661006dfb6..671a8baf9b 100644 --- a/packages/core/test/acceptance/listener_spec.ts +++ b/packages/core/test/acceptance/listener_spec.ts @@ -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: ` + + `, + }) + 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'); + }); }); diff --git a/packages/language-service/ivy/quick_info.ts b/packages/language-service/ivy/quick_info.ts index bafb2c814c..d1e9123e5a 100644 --- a/packages/language-service/ivy/quick_info.ts +++ b/packages/language-service/ivy/quick_info.ts @@ -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 { diff --git a/packages/language-service/ivy/utils.ts b/packages/language-service/ivy/utils.ts index 6888f2d32d..e2481dde42 100644 --- a/packages/language-service/ivy/utils.ts +++ b/packages/language-service/ivy/utils.ts @@ -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); } /** diff --git a/packages/language-service/src/expression_type.ts b/packages/language-service/src/expression_type.ts index 099ff99fcc..064a2caffd 100644 --- a/packages/language-service/src/expression_type.ts +++ b/packages/language-service/src/expression_type.ts @@ -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) { diff --git a/packages/language-service/src/expressions.ts b/packages/language-service/src/expressions.ts index ac0eb18ab4..5af34f8ba3 100644 --- a/packages/language-service/src/expressions.ts +++ b/packages/language-service/src/expressions.ts @@ -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) {},