Skip to content

Conversation

@crisbeto
Copy link
Member

Updates the compiler to allow users to use untagged template literals inside of our expression syntax (e.g. hello ${world}) which makes string concatenation a bit simpler and more convenient. Includes the following changes:

refactor(compiler): allow lexer to produce multiple tokens

Reworks the lexer's scanner to produce more than one token at a time. This can be useful for the cases where one token means the end of another one.

Also cleans up the scanner by making all non-essential methods private and using strict equality everywhere.

refactor(compiler): tokenize template literals

Reworks the lexer to produce tokens for template literal expressions.

refactor(compiler): produce AST for template literals

Updates the compiler to parse the template literal tokens into the new TemplateLiteral and TemplateLiteralElement AST nodes.

refactor(compiler): clean up tagged templates in output AST

Makes the following cleanups in the output AST:

  • The TemplateLiteral and TemplateLiteralElement nodes have been renamed to TemplateLiteralExpr and TemplateLiteralElementExpr respectively for consistency and to avoid overlaps with the expression AST nodes.
  • The TemplateLiteralExpr and TemplateLiteralElementExpr have been refactored to be Expressions for correctness. This involves updating some existing code.
  • The TaggedTemplateExpr has been renamed to TaggedTemplateLiteralExpr for consistency.

refactor(compiler-cli): handle template literals in ngtsc

Updates the translators that convert expression ASTs to account for template literals.

feat(compiler): support untagged template literals in expressions

Updates the compiler to support untagged template literals inside of the expression syntax (e.g. hello ${world}).

@crisbeto crisbeto added action: review The PR is still awaiting reviews from at least one requested reviewer target: minor This PR is targeted for the next minor release labels Dec 18, 2024
@angular-robot angular-robot bot added detected: feature PR contains a feature commit area: compiler Issues related to `ngc`, Angular's template compiler labels Dec 18, 2024
@ngbot ngbot bot added this to the Backlog milestone Dec 18, 2024
@angular-robot angular-robot bot added area: compiler Issues related to `ngc`, Angular's template compiler and removed area: compiler Issues related to `ngc`, Angular's template compiler labels Dec 18, 2024
@ngbot ngbot bot modified the milestone: Backlog Dec 18, 2024
@crisbeto crisbeto marked this pull request as ready for review December 18, 2024 09:03
@pullapprove pullapprove bot requested a review from clydin December 18, 2024 09:03
@crisbeto crisbeto requested review from JoostK and alxhub December 18, 2024 09:03
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 didn't bother implementing the downleveling code here, because all the browsers we support also support template literals natively.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense. There wasn't downleveling here before anyway right? (even with tagged template literals)

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 think for the tagged ones we only downleveled "known" ones like $localize and not in a standards-based way.

@jelbourn
Copy link
Member

As a follow up, we should make sure to update the docs: https://angular.dev/guide/templates/expression-syntax#unsupported-literals

@sheikalthaf
Copy link
Contributor

@crisbeto whether it will support the class with template literals?

<div class=`h-12 mt-4 ${cond() ? 'flex flex-col' : 'flex flex-row'} ${cond1() ? 'p-4' : 'p-2'}`></div>

@JeanMeche
Copy link
Member

@sheikalthaf This PR adds the support for template literals within angular expressions. Your example isn't an angular expression.

@sheikalthaf
Copy link
Contributor

sheikalthaf commented Dec 26, 2024

@JeanMeche then this will work right?

<div class="{{`h-12 mt-4 ${cond() ? 'flex flex-col' : 'flex flex-row'} ${cond1() ? 'p-4' : 'p-2'}`}}"></div>

I'm thinking of using this pattern alternative to ngClass. What's your thoughts on this?

@devversion devversion self-requested a review January 20, 2025 12:12
Copy link
Member

@devversion devversion left a comment

Choose a reason for hiding this comment

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

LGTM overall. Nice work!

I don't see any reasons why we wouldn't want to support this, as it's aligning us even further with TS/JS.

A few minor nits on the lexer portion, but the rest looks great

Copy link
Member

Choose a reason for hiding this comment

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

Why is there an expected error?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's because TS doesn't know that advance mutated this.peek so it narrows the value to chars.$$ and doesn't allow the comparison to chars.$LBRACE.

Copy link
Member

Choose a reason for hiding this comment

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

Might be worth a TODO

Copy link
Member

Choose a reason for hiding this comment

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

Kinda would have wished that we could advance through the template literal within the scanTemplateLiteralPart, but it's probably not possible.

Copy link
Member

Choose a reason for hiding this comment

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

I guess that's a related reason why we also have this.tokens now exposed, and .push-ed here?

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 exactly, the scanner isn't really set up to produce more than one token at a time.

Copy link
Member

Choose a reason for hiding this comment

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

Would it be possible/reasonable to keep it one token at a time (to not introduce some extra complexity around following the order etc.) and just basing the scans on state? (like you did with brace depth etc.)

Copy link
Member Author

Choose a reason for hiding this comment

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

We could do it with a flag, but I feel like that'll be less readable than just producing two tokens at once since we'd basically split up this logic across two places instead of keeping it together. I'm not too concerned about the tokens being exposed, because the lexer isn't exposed anywhere.

Copy link
Member

Choose a reason for hiding this comment

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

I felt like it was a little hard to reason about the order if are returning a token during scan, and suddenly we could push additional tokens in between. But it's not a super big of a deal. I do see your concerns as well.

Will defer to others here.

Copy link
Member

Choose a reason for hiding this comment

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

OOC: Do you know why this exists if we have the serializer already?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm good question. My guess is because we use the unparser in tests and it has a little bit of extra test-specific functionality around recording the spans. We may be able to consolidate them a bit though.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense. There wasn't downleveling here before anyway right? (even with tagged template literals)

Reworks the lexer's scanner to produce more than one token at a time. This can be useful for the cases where one token means the end of another one.

Also cleans up the scanner by making all non-essential methods private and using strict equality everywhere.
Reworks the lexer to produce tokens for template literal expressions.
Updates the compiler to parse the template literal tokens into the new `TemplateLiteral` and `TemplateLiteralElement` AST nodes.
Makes the following cleanups in the output AST:
* The `TemplateLiteral` and `TemplateLiteralElement` nodes have been renamed to `TemplateLiteralExpr` and `TemplateLiteralElementExpr` respectively for consistency and to avoid overlaps with the expression AST nodes.
* The `TemplateLiteralExpr` and `TemplateLiteralElementExpr` have been refactored to be `Expression`s for correctness. This involves updating some existing code.
* The `TaggedTemplateExpr` has been renamed to `TaggedTemplateLiteralExpr` for consistency.
Updates the translators that convert expression ASTs to account for template literals.
Updates the compiler to support untagged template literals inside of the expression syntax (e.g. ``hello ${world}``).
Copy link
Member Author

@crisbeto crisbeto left a comment

Choose a reason for hiding this comment

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

Feedback has been addressed.

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 think for the tagged ones we only downleveled "known" ones like $localize and not in a standards-based way.

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 exactly, the scanner isn't really set up to produce more than one token at a time.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's because TS doesn't know that advance mutated this.peek so it narrows the value to chars.$$ and doesn't allow the comparison to chars.$LBRACE.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm good question. My guess is because we use the unparser in tests and it has a little bit of extra test-specific functionality around recording the spans. We may be able to consolidate them a bit though.

@crisbeto crisbeto added action: merge The PR is ready for merge by the caretaker and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Jan 20, 2025
@AndrewKushnir
Copy link
Contributor

This PR was merged into the repository by commit fe8a683.

The changes were merged into the following branches: main

AndrewKushnir pushed a commit that referenced this pull request Jan 21, 2025
Reworks the lexer to produce tokens for template literal expressions.

PR Close #59230
AndrewKushnir pushed a commit that referenced this pull request Jan 21, 2025
Updates the compiler to parse the template literal tokens into the new `TemplateLiteral` and `TemplateLiteralElement` AST nodes.

PR Close #59230
AndrewKushnir pushed a commit that referenced this pull request Jan 21, 2025
Makes the following cleanups in the output AST:
* The `TemplateLiteral` and `TemplateLiteralElement` nodes have been renamed to `TemplateLiteralExpr` and `TemplateLiteralElementExpr` respectively for consistency and to avoid overlaps with the expression AST nodes.
* The `TemplateLiteralExpr` and `TemplateLiteralElementExpr` have been refactored to be `Expression`s for correctness. This involves updating some existing code.
* The `TaggedTemplateExpr` has been renamed to `TaggedTemplateLiteralExpr` for consistency.

PR Close #59230
AndrewKushnir pushed a commit that referenced this pull request Jan 21, 2025
Updates the translators that convert expression ASTs to account for template literals.

PR Close #59230
AndrewKushnir pushed a commit that referenced this pull request Jan 21, 2025
…9230)

Updates the compiler to support untagged template literals inside of the expression syntax (e.g. ``hello ${world}``).

PR Close #59230
Copy link
Member

@JoostK JoostK left a comment

Choose a reason for hiding this comment

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

Paul had asked for me to take a look at this, so leaving a few after-merge comments. The lexer does feel a bit awkward with the implicit multi-token production, but it doesn't hurt readability much so no big deal, I think

Comment on lines +682 to +685
const expression = i < ast.expressions.length ? ast.expressions[i] : null;
if (expression !== null) {
this.visit(expression, context);
}
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
const expression = i < ast.expressions.length ? ast.expressions[i] : null;
if (expression !== null) {
this.visit(expression, context);
}
if (i < ast.expressions.length) {
this.visit(ast.expressions[i], context);
}

return this.parseNoInterpolationTemplateLiteral(start);
} else if (this.next.isTemplateLiteralPart()) {
return this.parseTemplateLiteral();
} else if (this.next.isString() && this.next.kind === StringTokenKind.Plain) {
Copy link
Member

Choose a reason for hiding this comment

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

Is the Plain condition necessary here? It seems unexpected to me, seemingly allowing some strings to be reported as error?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's been some time since I wrote it, but I think the idea was to avoid confusion with the template literals in case we introduce some other string token. AFAIK in practice we can't actually hit that code path without it being StringTokenKind.Plain.

this.advance();
const expression = this.parsePipe();
if (expression instanceof EmptyExpr) {
this.error('Template literal interpolation cannot be empty');
Copy link
Member

@JoostK JoostK Jan 21, 2025

Choose a reason for hiding this comment

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

This error path doesn't collect the empty expression into expressions, yet a TemplateLiteral is being returned. I suspect this may cause the invariant elements.length === expressions.length + 1 to be violated.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm potentially, but I think that whatever is consuming the result will report the error.

Comment on lines +151 to +152
const expression = i < ast.expressions.length ? ast.expressions[i] : null;
if (expression !== null) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: same here for inlining the condition into the if

Comment on lines +224 to +225
const expression = i < ast.expressions.length ? ast.expressions[i] : null;
if (expression !== null) {
Copy link
Member

Choose a reason for hiding this comment

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

The nit here too

PrajaktaB27 pushed a commit to PrajaktaB27/angular that referenced this pull request Feb 7, 2025
…9230)

Reworks the lexer's scanner to produce more than one token at a time. This can be useful for the cases where one token means the end of another one.

Also cleans up the scanner by making all non-essential methods private and using strict equality everywhere.

PR Close angular#59230
PrajaktaB27 pushed a commit to PrajaktaB27/angular that referenced this pull request Feb 7, 2025
Reworks the lexer to produce tokens for template literal expressions.

PR Close angular#59230
PrajaktaB27 pushed a commit to PrajaktaB27/angular that referenced this pull request Feb 7, 2025
Updates the compiler to parse the template literal tokens into the new `TemplateLiteral` and `TemplateLiteralElement` AST nodes.

PR Close angular#59230
PrajaktaB27 pushed a commit to PrajaktaB27/angular that referenced this pull request Feb 7, 2025
…59230)

Makes the following cleanups in the output AST:
* The `TemplateLiteral` and `TemplateLiteralElement` nodes have been renamed to `TemplateLiteralExpr` and `TemplateLiteralElementExpr` respectively for consistency and to avoid overlaps with the expression AST nodes.
* The `TemplateLiteralExpr` and `TemplateLiteralElementExpr` have been refactored to be `Expression`s for correctness. This involves updating some existing code.
* The `TaggedTemplateExpr` has been renamed to `TaggedTemplateLiteralExpr` for consistency.

PR Close angular#59230
PrajaktaB27 pushed a commit to PrajaktaB27/angular that referenced this pull request Feb 7, 2025
)

Updates the translators that convert expression ASTs to account for template literals.

PR Close angular#59230
PrajaktaB27 pushed a commit to PrajaktaB27/angular that referenced this pull request Feb 7, 2025
…gular#59230)

Updates the compiler to support untagged template literals inside of the expression syntax (e.g. ``hello ${world}``).

PR Close angular#59230
@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 22, 2025
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 detected: feature PR contains a feature commit target: minor This PR is targeted for the next minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants