-
Notifications
You must be signed in to change notification settings - Fork 24.8k
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
fix(compiler): incorrectly interpreting some HostBinding names #40233
Conversation
c0b0c1f
to
2b1812a
Compare
// 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
2b1812a
to
ff5e46f
Compare
|
||
@Directive({selector: '[hostBindingDir]'}) | ||
export class HostBindingDir { | ||
@HostBinding('class.a') 'is-a': any; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 @@ | |||
|
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
ff5e46f
to
b4ebec6
Compare
b4ebec6
to
f7bdfce
Compare
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.
f7bdfce
to
a49b361
Compare
(note: |
Hi @crisbeto, I changed this to minor only because there is a merge conflict with the 11.0.x branch. |
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
Currently when analyzing the metadata of a directive, we bundle together the bindings from
host
and theHostBinding
andHostListener
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 ashost: {'[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 asclassProp('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 fromthis
. E.g. the@HostBinding('class.foo') public true = 1;
from above is now being treated ashost: {'[class.foo]': 'this.true'}
which further down the pipeline becomesclassProp('foo', ctx.true)
. This doesn't have any payload size implications for existing code, because we've always been prefixing implicit property reads withctx.
. 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.