Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(compiler): this.a should always refer to class property a #55183

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 5 additions & 15 deletions packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2709,21 +2709,11 @@ class TcbExpressionTranslator {
* context). This method assists in resolving those.
*/
protected resolve(ast: AST): ts.Expression | null {
// TODO: this is actually a bug, because `ImplicitReceiver` extends `ThisReceiver`. Consider a
// case when the explicit `this` read is inside a template with a context that also provides the
// variable name being read:
// ```
// <ng-template let-a>{{this.a}}</ng-template>
// ```
// Clearly, `this.a` should refer to the class property `a`. However, because of this code,
// `this.a` will refer to `let-a` on the template context.
//
// Note that the generated code is actually consistent with this bug. To fix it, we have to:
// - Check `!(ast.receiver instanceof ThisReceiver)` in this condition
// - Update `ingest.ts` in the Template Pipeline (see the corresponding comment)
// - Turn off legacy TemplateDefinitionBuilder
// - Fix g3, and release in a major version
if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver) {
if (
ast instanceof PropertyRead &&
ast.receiver instanceof ImplicitReceiver &&
!(ast.receiver instanceof ThisReceiver)
) {
// Try to resolve a bound target for this expression. If no such target is available, then
// the expression is referencing the top-level component context. In that case, `null` is
// returned here to let it fall through resolution so it will be caught when the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1249,13 +1249,13 @@ import * as i0 from "@angular/core";
export class MyComponent {
}
MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: '<ng-template let-a [ngIf]="true">{{this.a}}</ng-template>', isInline: true });
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: '<ng-template let-a [ngIf]="true">{{a}}</ng-template>', isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
type: Component,
args: [{
selector: 'my-component',
standalone: true,
template: '<ng-template let-a [ngIf]="true">{{this.a}}</ng-template>',
template: '<ng-template let-a [ngIf]="true">{{a}}</ng-template>',
}]
}] });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ MyComponent_ng_template_0_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵtext(0);
} if (rf & 2) {
// NOTE: The fact that `this.` still refers to template context is a TDB and TCB bug; we should fix this eventually.
const $a_r1$ = ctx.$implicit;
i0.ɵɵtextInterpolate($a_r1$);
}
Expand All @@ -16,4 +15,4 @@ function MyComponent_Template(rf, ctx) {
} if (rf & 2) {
i0.ɵɵproperty("ngIf", true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {Component} from '@angular/core';
@Component({
selector: 'my-component',
standalone: true,
template: '<ng-template let-a [ngIf]="true">{{this.a}}</ng-template>',
template: '<ng-template let-a [ngIf]="true">{{a}}</ng-template>',
})
export class MyComponent {
p1!: any;
Expand Down
10 changes: 1 addition & 9 deletions packages/compiler/src/render3/view/t2_binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,21 +922,13 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
private maybeMap(ast: PropertyRead | SafePropertyRead | PropertyWrite, name: string): void {
// If the receiver of the expression isn't the `ImplicitReceiver`, this isn't the root of an
// `AST` expression that maps to a `Variable` or `Reference`.
if (!(ast.receiver instanceof ImplicitReceiver)) {
if (!(ast.receiver instanceof ImplicitReceiver) || ast.receiver instanceof ThisReceiver) {
return;
}

// Check whether the name exists in the current scope. If so, map it. Otherwise, the name is
// probably a property on the top-level component context.
const target = this.scope.lookup(name);

// It's not allowed to read template entities via `this`, however it previously worked by
// accident (see #55115). Since `@let` declarations are new, we can fix it from the beginning,
// whereas pre-existing template entities will be fixed in #55115.
if (target instanceof LetDeclaration && ast.receiver instanceof ThisReceiver) {
return;
}

if (target !== null) {
this.bindings.set(ast, target);
}
Expand Down
22 changes: 1 addition & 21 deletions packages/compiler/src/template/pipeline/src/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1003,30 +1003,10 @@ function convertAst(
if (ast instanceof e.ASTWithSource) {
return convertAst(ast.ast, job, baseSourceSpan);
} else if (ast instanceof e.PropertyRead) {
const isThisReceiver = ast.receiver instanceof e.ThisReceiver;
// Whether this is an implicit receiver, *excluding* explicit reads of `this`.
const isImplicitReceiver =
ast.receiver instanceof e.ImplicitReceiver && !(ast.receiver instanceof e.ThisReceiver);
// Whether the name of the read is a node that should be never retain its explicit this
// receiver.
const isSpecialNode = ast.name === '$any' || ast.name === '$event';
// TODO: The most sensible condition here would be simply `isImplicitReceiver`, to convert only
// actual implicit `this` reads, and not explicit ones. However, TemplateDefinitionBuilder (and
// the Typecheck block!) both have the same bug, in which they also consider explicit `this`
// reads to be implicit. This causes problems when the explicit `this` read is inside a
// template with a context that also provides the variable name being read:
// ```
// <ng-template let-a>{{this.a}}</ng-template>
// ```
// The whole point of the explicit `this` was to access the class property, but TDB and the
// current TCB treat the read as implicit, and give you the context property instead!
//
// For now, we emulate this old behavior by aggressively converting explicit reads to to
// implicit reads, except for the special cases that TDB and the current TCB protect. However,
// it would be an improvement to fix this.
//
// See also the corresponding comment for the TCB, in `type_check_block.ts`.
if (isImplicitReceiver || (isThisReceiver && !isSpecialNode)) {
if (isImplicitReceiver) {
return new ir.LexicalReadExpr(ast.name);
} else {
return new o.ReadPropExpr(
Expand Down
29 changes: 29 additions & 0 deletions packages/compiler/test/render3/view/binding_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,35 @@ describe('t2 binding', () => {
expect((target as a.LetDeclaration)?.name).toBe('value');
});

it('should not resolve a `this` access to a template reference', () => {
const template = parseTemplate(
`
<input #value>
{{this.value}}
`,
'',
);
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
const res = binder.bind({template: template.nodes});
const interpolationWrapper = (template.nodes[1] as a.BoundText).value as e.ASTWithSource;
const propertyRead = (interpolationWrapper.ast as e.Interpolation).expressions[0];
const target = res.getExpressionTarget(propertyRead);

expect(target).toBe(null);
});

it('should not resolve a `this` access to a template variable', () => {
const template = parseTemplate(`<ng-template let-value>{{this.value}}</ng-template>`, '');
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
const res = binder.bind({template: template.nodes});
const templateNode = template.nodes[0] as a.Template;
const interpolationWrapper = (templateNode.children[0] as a.BoundText).value as e.ASTWithSource;
const propertyRead = (interpolationWrapper.ast as e.Interpolation).expressions[0];
const target = res.getExpressionTarget(propertyRead);

expect(target).toBe(null);
});

it('should not resolve a `this` access to a `@let` declaration', () => {
const template = parseTemplate(
`
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/acceptance/embedded_views_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('embedded views', () => {
});

it('should resolve template input variables through the implicit receiver', () => {
@Component({template: `<ng-template let-a [ngIf]="true">{{this.a}}</ng-template>`})
@Component({template: `<ng-template let-a [ngIf]="true">{{a}}</ng-template>`})
class TestCmp {}

TestBed.configureTestingModule({declarations: [TestCmp]});
Expand Down