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

feat(template-compiler): experimental template expressions #3376

Merged
merged 3 commits into from
Mar 15, 2023

Conversation

divmain
Copy link
Contributor

@divmain divmain commented Mar 2, 2023

Details

Note: Because this PR adds a lot of tests, I've crafted commits so that you can easily filter the diff in Files changed view to functional changes only (or tests, if that's what interests you).

This PR introduces complex template expressions.

<div attr={value}></div>
<div>{value[currentIdx]}</div>
<div attr={foo ? bar}></div>
<div>{foo?.bar}</div>

Whereas lone identifiers are transformed into corresponding field-access member expressions, parameter variables are not be transformed in this way. For example, the following transformations do not happen:

{(foo) => foo && bar} -> {(foo) => $cmp.foo && $cmp.bar}
{(foo) => foo && bar} -> {($cmp.foo) => foo && $cmp.bar}
{(foo) => foo && bar} -> {($cmp.foo) => $cmp.foo && $cmp.bar}

Instead, the scopes are respected: {(foo) => foo && $cmp.bar}. Similar checks occur for local identifiers introduced via for:each or similar.

This feature is implemented behind a flag, which strongly influenced some some implementation choices.

  • Most of the code is self-contained in new TS modules.
  • // TODO [#3370] comments are used liberally throughout, wherever code needs to be stripped (if template expressions are not adopted) or where changes need to be made (if template expressions are adopted).
  • There should be zero changes to internal IR or external outputs when the flag is disabled. The only snapshot that needed to be updated was related to the newly-exposed flag itself.

Some additional notes:

  • I'm currently tracking the work to be done in pr.todo. This file will be deleted prior to merge.
  • There are no new tests. These will be added in a follow-up PR with the experimentalComplexExpressions flag enabled. Added a number of tests for coverage purposes and sanity. Will be adding more tests as a follow-up.

Does this pull request introduce a breaking change?

  • ✅ No, it does not introduce a breaking change.

Does this pull request introduce an observable change?

  • ✅ No, it does not introduce an observable change.

GUS work item

W-12177613

@divmain divmain force-pushed the dbustad/template-expressions branch from 6224ace to 1e27a83 Compare March 2, 2023 08:44
@divmain divmain changed the title feat: experimental template expressions feat(template-compiler): experimental template expressions Mar 2, 2023

// Start parsing immediately after the opening curly brace.
const estreeNode = parseExpressionAt(html, javascriptExprStart, {
ecmaVersion: 2022,
Copy link
Contributor Author

@divmain divmain Mar 2, 2023

Choose a reason for hiding this comment

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

ES2022 will be parsed, but none of the newly introduced syntax is permitted. This will just make for better error messages; the component author will be notified about their use of disallowed syntax (if they try to use it) rather than seeing an arcane parsing error.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Are we going to need to periodically update this? Can we just use "latest"? (Which, presumably, will bump when we bump our acorn dep.)

Copy link
Contributor Author

@divmain divmain Mar 2, 2023

Choose a reason for hiding this comment

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

Good question. With the current implementation, the AST is validated using a blocklist for disallowed node types. If we used "latest" here, we'd probably want to invert that to avoid permitting new JS syntax proposals. However, the validation logic would be much more complex if using an allowlist vs blocklist.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added TMPL_EXPR_ECMASCRIPT_EDITION constant to ensure consistency

// TODO [#3370]: If complex template expressions are adopted, `preparsedJsExpressions`
// should be checked. However, to avoid significant complications in the internal types,
// arising from supporting both implementations simultaneously, we will re-parse the
// expression here when `ctx.config.experimentalComplexExpressions` is true.
if (isExpression(value) && !escapedExpression) {
attrValue = parseExpression(ctx, value, location);
Copy link
Contributor Author

@divmain divmain Mar 2, 2023

Choose a reason for hiding this comment

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

This isn't ideal. The expression at that location has already been parsed (during tokenization) when the experimentalComplexExpressions flag is enabled. Ideally, we should check the preparsedJsExpressions to find the matching one, and use that for the attrValue. However, this would require changes to internal types (like that of attrValue) that cascade into messy changes in multiple places.

To be clear, these are solely type issues. However, to avoid making messy changes across our internal types, I opted to parse the expression a second time, here. This seemed a decent tradeoff for a pilot feature - and can & should be addressed when the flag goes away. I'm open to going the other way if there are strong opinions in favor.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I assume this is why parseExpressionAt is called twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.

pr.todo Outdated Show resolved Hide resolved
packages/@lwc/template-compiler/src/codegen/expression.ts Outdated Show resolved Hide resolved

// Start parsing immediately after the opening curly brace.
const estreeNode = parseExpressionAt(html, javascriptExprStart, {
ecmaVersion: 2022,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are we going to need to periodically update this? Can we just use "latest"? (Which, presumably, will bump when we bump our acorn dep.)

packages/@lwc/template-compiler/src/parser/expression.ts Outdated Show resolved Hide resolved
// TODO [#3370]: If complex template expressions are adopted, `preparsedJsExpressions`
// should be checked. However, to avoid significant complications in the internal types,
// arising from supporting both implementations simultaneously, we will re-parse the
// expression here when `ctx.config.experimentalComplexExpressions` is true.
if (isExpression(value) && !escapedExpression) {
attrValue = parseExpression(ctx, value, location);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I assume this is why parseExpressionAt is called twice?

const POTENTIAL_EXPRESSION_RE = /^.?{.+}.*$/;
const WHITESPACES_RE = /\s/;

export function isExpression(source: string): boolean {
return !!source.match(VALID_EXPRESSION_RE);
return source[0] === '{' && source.slice(-1) === '}';
Copy link
Contributor Author

@divmain divmain Mar 6, 2023

Choose a reason for hiding this comment

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

I don't believe this should be an observable/breaking change. The only difference between these two conditions is whether source contains a newline. However, unquoted attribute values (in HTML) cannot contain a new line - so the two are functionally equivalent with the flag disabled.

With the flag enabled, they are not functionally equivalent - the new condition will permit a newline in the attribute value, which is the desired behavior.

Commenting for visibility, in case there is something I'm not considering.

@divmain divmain force-pushed the dbustad/template-expressions branch from 296d82d to cde0acc Compare March 7, 2023 08:57
@@ -61,6 +62,9 @@ export interface MemberExpression extends BaseNode {

export type Expression = Identifier | MemberExpression;

// TODO [#3370]: remove experimental template expression flag
export type ComplexExpression = AcornNode & { value?: any };
Copy link
Member

Choose a reason for hiding this comment

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

The AcornNode type is quite opaque. As far as I understand Acorn return an ESTree-compliant AST. Would it make more sense to have it typed as an ESTree node?

There is also some overlap between the existing Identifier and Expression template AST node, would it make sense to merge those type definitions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I experimented with this a bit, and any changes to this type resulted in really messy changes required across the codebase. I'm going to leave it for now. However, I've updated the comment to reflect that ComplexExpression should be replaced with ESTree's Node if/when the flag is removed.

ParserDiagnostics.INVALID_EXPR_COMMENTS_DISALLOWED
);
invariant(
!STATEMENT_TYPES.has(node.type),
Copy link
Member

Choose a reason for hiding this comment

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

Instead of blocking statements nodes, I think it is more future-proof to allow list expressions. This way we would have control over which expressions we allow in the template as new expression types are added to the language.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I originally created an allowlist for node types, but it was messier to translate the decisions in the design doc this into code. I'll probably revisit, but likely in a follow-up PR.

packages/@lwc/template-compiler/src/parser/index.ts Outdated Show resolved Hide resolved
@divmain divmain force-pushed the dbustad/template-expressions branch 6 times, most recently from 7b66bcb to d77c47a Compare March 11, 2023 06:29
export function isExpression(node: Expression | Literal): node is Expression {
return node.type === 'Identifier' || node.type === 'MemberExpression';
export function isExpression(node: BaseNode | Literal): node is Expression {
return node.type !== 'Literal';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In a previous iteration of this PR, isExpression had been changed to accept the template expressions flag as its second argument. This led to a deluge of one-line changes where the flag had to be passed in.

To avoid this, I've updated the implementation here. With the flag disabled, the node will have type Identifer, MemberExpression, or Literal. Thus this new condition should be equivalent. The condition also works for the flag-enabled scenario.

@divmain divmain force-pushed the dbustad/template-expressions branch from d77c47a to 95b0a46 Compare March 11, 2023 07:11
@divmain divmain force-pushed the dbustad/template-expressions branch from 95b0a46 to c6d68b2 Compare March 11, 2023 07:25
@divmain divmain force-pushed the dbustad/template-expressions branch 2 times, most recently from b014017 to 6076891 Compare March 11, 2023 10:13
Copy link
Contributor

@ravijayaramappa ravijayaramappa left a comment

Choose a reason for hiding this comment

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

First round of review. Reviewing tests now.

"warnings": [
{
"code": 1052,
"message": "Unexpected compilation error: LWC1052: Error parsing attribute: Unexpected token (3:31)",
Copy link
Contributor

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 really useful. The location (3:31) isn't valid as well.

Copy link
Contributor Author

@divmain divmain Mar 14, 2023

Choose a reason for hiding this comment

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

(3:31) refers to throw - that seems correct to me. As for the error message, I'm unsure how to improve it. throw is not valid in expressions; it is only parsed correctly as a statement. Because we've disallowed statements, I can't test that directly. And because the lexer encountered a reserved keyword in an expression, "Unexpected token" is correct.

I'm unsure of the best way to resolve this. I suppose when "Unexpected tokenis encountered, we could look at the indicated location to see if it is athrow` string, but that feels somewhat hacky.

Copy link
Contributor

@ravijayaramappa ravijayaramappa left a comment

Choose a reason for hiding this comment

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

About tests:

  1. Please organize the tests under @lwc/template-compiler/src/__tests__/fixtures/expression-complex into logical folders. The flat hierarchy makes it hard to find a specific tests. A suggestion: valid/, invalid/, invalid/throws/, valid/arrow/ etc.
  2. Missing test cases
    • Using complex expression without experimentalComplexExpressions flag enabled should expect diagnostics
    • Resolution of variables that are in local scope: identifier declared in for:item, lwc:data

@divmain divmain force-pushed the dbustad/template-expressions branch from 6076891 to a04de93 Compare March 14, 2023 23:18
@divmain
Copy link
Contributor Author

divmain commented Mar 15, 2023

To avoid ballooning this PR further, I'm planning a follow-up PR with additional integration tests with other LWC features like for:each. I believe all other feedback has been addressed.

@divmain divmain merged commit 56a191a into master Mar 15, 2023
@divmain divmain deleted the dbustad/template-expressions branch March 15, 2023 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants