Skip to content
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

fix(compiler): incorrectly interpreting some HostBinding names #40233

Closed

Conversation

crisbeto
Copy link
Member

@crisbeto crisbeto commented Dec 22, 2020

Currently when analyzing the metadata of a directive, we bundle together the bindings from host and the HostBinding and HostListener together. This can become a problem later on in the compilation pipeline, because we try to evaluate the value of the binding, causing something like @HostBinding('class.foo') public true = 1; to be treated the same as host: {'[class.foo]': 'true'}.

While looking into the issue, I noticed another one that is closely related: we weren't treating quoted property names correctly. E.g. @HostBinding('class.foo') public "foo-bar" = 1; was being interpreted as classProp('foo', ctx.foo - ctx.bar) due to the same issue where property names were being evaluated.

These changes resolve both of the issues by treating all HostBinding instances as if they're reading the property from this. E.g. the @HostBinding('class.foo') public true = 1; from above is now being treated as host: {'[class.foo]': 'this.true'} which further down the pipeline becomes classProp('foo', ctx.true). This doesn't have any payload size implications for existing code, because we've always been prefixing implicit property reads with ctx.. If the property doesn't have an identifier that can be read using dotted access, we convert it to a quoted one (e.g. classProp('foo', ctx['is-foo'])).

Fixes #40220.
Fixes #40230.
Fixes #18698.

@google-cla google-cla bot added the cla: yes label Dec 22, 2020
@crisbeto crisbeto force-pushed the 40220-compiler-host-binding-names branch 2 times, most recently from c0b0c1f to 2b1812a Compare December 22, 2020 11:54
// through `this` so that further down the line it can't be confused for a literal value
// (e.g. if there's a property called `true`). There is no size penalty, because all
// values (except literals) are converted to `ctx.propName` eventually.
bindings.properties[hostPropertyName] = getSafePropertyAccessString('this', member.name);
Copy link
Member Author

@crisbeto crisbeto Dec 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for reviewer: I was conflicted between doing this (converting all accesses to go through this) versus changing the extracted information from Record<string, string> to Record<string, {name: string; canBePrimitive: boolean}> and then changing the places where we evaluate the expression to account for the canBePrimitive flag. I decided against it, because it seemed more error-prone. I'm open towards changing it, if you think that the this access is problematic.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally feel that we should avoid this. I personally would prefer something more akin to what you mentioned with Record, but I can be convinced otherwise.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also prefer if we could avoid this this here. It would probably be better if we were to build an output AST expression instead of synthesizing an expression string, so that we forego the dependency on the binding parser. But I recognize that doing so looks to be easier said than done, as the binding parser implements additional logic that is still relevant for such bindings.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, my concern is that it'll involve adding a bunch of random IFs everywhere just to handle this one edge case, whereas prefixing it with this. reflects what actually happens at runtime. I was looking to change the logic in BindingParser.parsePropertyBinding and Parser.parseSimpleBinding so that they always return a PropertyRead, but getting these classes to know when this should happen would involve adding that extra flag to the extracted information and the data format appears to be used by VE to generate metadata.json files as well.

It's worth noting this isn't the first place where we're constructing the expression as a string like this. If you look a bit further down in the same file, the way we handle @HostListener is by creating an expression like ${member.name}(${args.join(',')}) and then evaluating it.

As for building an output AST: I considered doing it, but then I'd also have to bring in an AbstractEmitterVisitor which seemed like it would be wasteful, considering that we'd be doing it for each @HostBinding and it eventually hits the same escapeIdentifier function that I'm using inside getSafePropertyAccessString.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think that using this here is very reasonable.

@JoostK and @jessicajaniuk - you say you prefer not to use it, but did not give the reason for why it is bad.

The only concern I can think of is that we are manually constructing a string expression, but as @crisbeto points out there is already a precedent for this. I feel that this. actually states quite clearly what we are trying to achieve here, whereas a more complex approach avoid this string expression would be harder to comprehend and maintain IMO.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not that I don't like the this, it's the creation of an expression string that I'd rather avoid. But I do recognize the difficulty of avoiding it at the moment, so I'd be okay with keeping it as is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, this just adds a lot of confusion to follow which this scope I may be in at any given time. I'm fine with leaving it as is here given that it seems like any alternative would be way more complicated.

@crisbeto crisbeto force-pushed the 40220-compiler-host-binding-names branch from 2b1812a to ff5e46f Compare December 22, 2020 12:05
@crisbeto crisbeto marked this pull request as ready for review December 22, 2020 13:01
@pullapprove pullapprove bot added area: compiler Issues related to `ngc`, Angular's template compiler area: core Issues related to the framework runtime labels Dec 22, 2020
@ngbot ngbot bot modified the milestone: Backlog Dec 22, 2020
@crisbeto crisbeto added action: review The PR is still awaiting reviews from at least one requested reviewer target: patch This PR is targeted for the next patch release type: bug/fix labels Dec 22, 2020
packages/compiler/src/render3/util.ts Show resolved Hide resolved

@Directive({selector: '[hostBindingDir]'})
export class HostBindingDir {
@HostBinding('class.a') 'is-a': any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way around the any usage here. Can we make it unknown instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured that it doesn't really matter since it's a test to verify the compiler output. I'll type it better when addressing the rest of the feedback.

@@ -0,0 +1,11 @@

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) There's a leading space here.

}
})
export class HostBindingDir {
@HostBinding('class.c') true: any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if we can avoid adding additional any types. Can unknown work here, instead?

// through `this` so that further down the line it can't be confused for a literal value
// (e.g. if there's a property called `true`). There is no size penalty, because all
// values (except literals) are converted to `ctx.propName` eventually.
bindings.properties[hostPropertyName] = getSafePropertyAccessString('this', member.name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally feel that we should avoid this. I personally would prefer something more akin to what you mentioned with Record, but I can be convinced otherwise.

@petebacondarwin
Copy link
Member

@crisbeto - can you mark that this PR will fix #18698 too?

@crisbeto crisbeto force-pushed the 40220-compiler-host-binding-names branch from b4ebec6 to f7bdfce Compare December 31, 2020 16:32
Currently when analyzing the metadata of a directive, we bundle together the bindings from `host`
and the `HostBinding` and `HostListener` together. This can become a problem later on in the
compilation pipeline, because we try to evaluate the value of the binding, causing something like
`@HostBinding('class.foo') public true = 1;` to be treated the same as
`host: {'[class.foo]': 'true'}`.

While looking into the issue, I noticed another one that is closely related: we weren't treating
quoted property names correctly. E.g. `@HostBinding('class.foo') public "foo-bar" = 1;` was being
interpreted as `classProp('foo', ctx.foo - ctx.bar)` due to the same issue where property names
were being evaluated.

These changes resolve both of the issues by treating all `HostBinding` instance as if they're
reading the property from `this`. E.g. the `@HostBinding('class.foo') public true = 1;` from above
is now being treated as `host: {'[class.foo]': 'this.true'}` which further down the pipeline becomes
`classProp('foo', ctx.true)`. This doesn't have any payload size implications for existing code,
because we've always been prefixing implicit property reads with `ctx.`. If the property doesn't
have an identifier that can be read using dotted access, we convert it to a quoted one (e.g.
`classProp('foo', ctx['is-foo']))`.

Fixes angular#40220.
Fixes angular#40230.
Fixes angular#18698.
@crisbeto crisbeto force-pushed the 40220-compiler-host-binding-names branch from f7bdfce to a49b361 Compare January 6, 2021 20:36
@crisbeto crisbeto added action: merge The PR is ready for merge by the caretaker action: presubmit The PR is in need of a google3 presubmit and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Jan 6, 2021
@AndrewKushnir
Copy link
Contributor

AndrewKushnir commented Jan 7, 2021

Presubmit.

(note: fw-core approval is still needed, re-requested review from Jessica)

@AndrewKushnir AndrewKushnir removed the action: presubmit The PR is in need of a google3 presubmit label Jan 7, 2021
@atscott atscott added target: minor This PR is targeted for the next minor release and removed target: patch This PR is targeted for the next patch release labels Jan 7, 2021
@atscott
Copy link
Contributor

atscott commented Jan 7, 2021

Hi @crisbeto, I changed this to minor only because there is a merge conflict with the 11.0.x branch.

@atscott atscott closed this in 1045465 Jan 7, 2021
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Feb 7, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: compiler Issues related to `ngc`, Angular's template compiler area: core Issues related to the framework runtime cla: yes target: minor This PR is targeted for the next minor release type: bug/fix
Projects
None yet
6 participants