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): allow banana-in-a-box bindings to end with non-null assertion #37809

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

@JoostK
Copy link
Member

@JoostK JoostK commented Jun 28, 2020

For two-way-bindings that use the banana-in-a-box syntax, the compiler
synthesizes an event assignment expression from the primary expression.
It is valid for the primary expression to be terminated by the non-null
operator, however naive string substitution is used for the synthesized
expression, such that the ! would immediately precede the = token,
resulting in the valid != operator token. The expression would still
parse correctly but it doesn't implement the proper semantics, resulting
in incorrect runtime behavior.

Changing the expression substitution to force a space between the
primary expression and the assignment avoids this mistake, but it
uncovers a new issue. The grammar does not allow for the LHS of an
assignment to be the non-null operator, so the synthesized expression
would fail to parse. To alleviate this, the synthesized expression is
parsed with a special parser flag to allow for this syntax.

Fixes #36551


Implementation note: it is up for debate whether extending the grammar to accept more expressions as LHS of an assignment would be desirable for regular output bindings. I choose not to change the grammar at this moment, as it seems like a change that needs to be introduced in at least a minor. Additionally, I feel like the whole expression substitution is a bit of a hack in the first place, so an alternative way of handling this case may be desirable in the future. To allow for this cleanup, I feel like it's best to wait for VE removal as the parser infrastructure is currently quite convoluted.

Copy link
Contributor

@ayazhafiz ayazhafiz left a comment

Nice fix!

@@ -150,7 +169,7 @@ export class Parser {
const tokens = this._lexer.tokenize(templateValue);
const parser = new _ParseAST(
templateValue, templateUrl, absoluteValueOffset, tokens, templateValue.length,
false /* parseAction */, this.errors, 0 /* relative offset */);
ParseFlags.None /* parseFlags */, this.errors, 0 /* relative offset */);
Copy link
Contributor

@ayazhafiz ayazhafiz Jun 29, 2020

Choose a reason for hiding this comment

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

I think the comment is unnecessary now

Suggested change
ParseFlags.None /* parseFlags */, this.errors, 0 /* relative offset */);
ParseFlags.None, this.errors, 0 /* relative offset */);

expect(() => parse('<div (prop)="v! = $event"></div>', []))
.toThrowError(/Unexpected token '=' at column 4/);
Copy link
Contributor

@ayazhafiz ayazhafiz Jun 29, 2020

Choose a reason for hiding this comment

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

This error message isn't incorrect, but I think I better message would mention that the non-null assertion operator cannot appear on the LHS of an assignment expression. If this sounds reasonable, could you add a todo here?

Copy link
Member Author

@JoostK JoostK Jul 4, 2020

Choose a reason for hiding this comment

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

Agreed, I added a note here. It's really not that hard to have a specialized error message, however I'd rather not touch this at the moment (especially if we do want to support this syntax)

Copy link
Contributor

@ayazhafiz ayazhafiz Jul 6, 2020

Choose a reason for hiding this comment

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

yep, makes sense

@JoostK JoostK force-pushed the non-null-two-way-bindings branch from 1abd5c6 to 491678d Jul 4, 2020
alxhub
alxhub approved these changes Jul 10, 2020
@alxhub
Copy link
Contributor

@alxhub alxhub commented Jul 10, 2020

@JoostK JoostK force-pushed the non-null-two-way-bindings branch from 491678d to 4e7a212 Aug 3, 2020
@alxhub
Copy link
Contributor

@alxhub alxhub commented Aug 3, 2020

@alxhub
Copy link
Contributor

@alxhub alxhub commented Aug 4, 2020

The g3 failure here is legitimate, as this does break codelyzer internally, due to the extra parameter added to parseAction. Let's sync up on this.

@alxhub
Copy link
Contributor

@alxhub alxhub commented Aug 6, 2020

@JoostK we can fix this internally just by patching codelyzer. Can you test this change with angular-eslint (the spiritual successor) and verify that no external fixes are needed there?

@jessicajaniuk
Copy link
Contributor

@jessicajaniuk jessicajaniuk commented Apr 28, 2021

@JoostK Looks like this PR stalled. Any idea what the status is here and whether we still plan on landing it?

@JoostK JoostK force-pushed the non-null-two-way-bindings branch from 4e7a212 to d241b0b May 4, 2021
@JoostK
Copy link
Member Author

@JoostK JoostK commented May 4, 2021

Picking this one up again. This was rebased on latest master and I tested the build artefacts in angular-eslint (on its master branch after updating to v12 RC releases) and there were no issues there (which is to be expected as they don't touch lower level parse API, unlike Codelyzer).

@ngbot ngbot bot removed this from the needsTriage milestone Jul 29, 2021
@ngbot ngbot bot added this to the Backlog milestone Jul 29, 2021
@JoostK
Copy link
Member Author

@JoostK JoostK commented Jul 29, 2021

Blocked on a g3 sync issue as this change is incompatible with Codelyzer.

@JoostK JoostK force-pushed the non-null-two-way-bindings branch from d241b0b to 8f523cb Oct 31, 2021
@@ -399,7 +418,7 @@ export class _ParseAST {

constructor(
public input: string, public location: string, public absoluteOffset: number,
public tokens: Token[], public inputLength: number, public parseAction: boolean,
public tokens: Token[], public inputLength: number, public parseFlags: number,
Copy link
Member

@petebacondarwin petebacondarwin Oct 31, 2021

Choose a reason for hiding this comment

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

Suggested change
public tokens: Token[], public inputLength: number, public parseFlags: number,
public tokens: Token[], public inputLength: number, public parseFlags: ParseFlags,

??

Copy link
Member Author

@JoostK JoostK Oct 31, 2021

Choose a reason for hiding this comment

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

Thanks, good catch!

None = 0,

/**
* Whether an output binding is parsed.
Copy link
Member

@petebacondarwin petebacondarwin Oct 31, 2021

Choose a reason for hiding this comment

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

Suggested change
* Whether an output binding is parsed.
* Whether an output binding is being parsed.

Action = 1 << 0,

/**
* Whether an assignment event is parsed, i.e. an expression originating from two-way-binding
Copy link
Member

@petebacondarwin petebacondarwin Oct 31, 2021

Choose a reason for hiding this comment

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

Suggested change
* Whether an assignment event is parsed, i.e. an expression originating from two-way-binding
* Whether an assignment event is being parsed, i.e. an expression originating from two-way-binding

@AndrewKushnir
Copy link
Contributor

@AndrewKushnir AndrewKushnir commented Nov 4, 2021

@@ -29,6 +29,21 @@ export class TemplateBindingParseResult {
public errors: ParserError[]) {}
}

export const enum ParseFlags {
Copy link
Contributor

@AndrewKushnir AndrewKushnir Nov 4, 2021

Choose a reason for hiding this comment

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

Could you please add a quick comment above this enum on what it represents?

if ((this.parseFlags & ParseFlags.AssignmentEvent) && this.next.isOperator('!') &&
this.peek(1).isOperator('=')) {
this.advance();
this.advance();
Copy link
Contributor

@AndrewKushnir AndrewKushnir Nov 4, 2021

Choose a reason for hiding this comment

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

Could you plz add a quick comment here to provide more context on why we have 2 advance calls?

@angular angular deleted a comment from dram2040 Nov 5, 2021
@angular angular deleted a comment from dram2040 Nov 5, 2021
@AndrewKushnir
Copy link
Contributor

@AndrewKushnir AndrewKushnir commented Nov 5, 2021

@AndrewKushnir
Copy link
Contributor

@AndrewKushnir AndrewKushnir commented Nov 5, 2021

@JoostK FYI the most recent TGP is fully "green" (internal-only link) with this change: there are no issues with Codelyzer. I don't have an access to Alex's test runs (the results are no longer available), but I believe TGP should've triggered Codelyzer-related targets.

@alxhub I've also looked at the Codelyzer folder and didn't notice any updates that make it compatible with this change. So I'm wondering if we should run more tests or we can proceed with caution (merge and sync the change as a separate commit, be ready to rollback). What do you think?

JoostK added 3 commits Nov 5, 2021
…sertion

For two-way-bindings that use the banana-in-a-box syntax, the compiler
synthesizes an event assignment expression from the primary expression.
It is valid for the primary expression to be terminated by the non-null
operator, however naive string substitution is used for the synthesized
expression, such that the `!` would immediately precede the `=` token,
resulting in the valid `!=` operator token. The expression would still
parse correctly but it doesn't implement the proper semantics, resulting
in incorrect runtime behavior.

Changing the expression substitution to force a space between the
primary expression and the assignment avoids this mistake, but it
uncovers a new issue. The grammar does not allow for the LHS of an
assignment to be the non-null operator, so the synthesized expression
would fail to parse. To alleviate this, the synthesized expression is
parsed with a special parser flag to allow for this syntax.

Fixes angular#36551
@JoostK JoostK force-pushed the non-null-two-way-bindings branch from 5c1335a to 8b4bf90 Nov 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment