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

Update: support class fields (refs eslint/eslint#14343) #69

Merged
merged 12 commits into from Jul 2, 2021
Merged

Conversation

mysticatea
Copy link
Member

WIP.

This PR adds the class field support.


Remaining steps:

  1. Release eslint-visitor-keys.
  2. Merge Update: add class fields (refs eslint/eslint#14343) espree#486 and release espree.
  3. Update the version range of eslint-visitor-keys and espree in package.json.

@nzakas nzakas marked this pull request as draft April 29, 2021 00:38
@nzakas
Copy link
Member

nzakas commented Apr 29, 2021

Switching to a draft because this is WIP

@sanex3339
Copy link
Contributor

espree just has been released as 8.0.0-beta.0

@sanex3339
Copy link
Contributor

sanex3339 commented Jun 14, 2021

@mysticatea Hi. Any date when this will be updated and merged? Or we have to wait until a stable espree release?

@nzakas
Copy link
Member

nzakas commented Jun 14, 2021

@sanex3339 please stop asking for updates on everything. Updates are always posted on the issues/PRs themselves when there is something to share.

We do not have set timelines for releasing features. As everyone is working in their spare time, features get done as people are available to work on them.

This PR cannot move forward until the final Espree v8.0.0 is released, as already indicated. The beta release does not unblock this PR.

@sanex3339
Copy link
Contributor

@nzakas thank you for the clarification, anyway I don't see any problem to ask (especially if I did it once in a different repository before)

@nzakas
Copy link
Member

nzakas commented Jun 14, 2021

@sanex3339 The reason not to ask is because I am asking you not to. We get a lot of notifications every day, and as I already said, we always post when there are updates and there are no schedules to adhere to, so you are just creating more notifications for us to deal with. Please be patient and subscribe to the issues for updates. Thanks.

package.json Outdated Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved
tests/class-fields.js Show resolved Hide resolved
@nzakas nzakas marked this pull request as ready for review June 30, 2021 02:49
@nzakas
Copy link
Member

nzakas commented Jun 30, 2021

Espree v8.0.0 has been released and eslint-visitor-keys v3.0.0 has been released. I've updated the listings in package.json to reflect the new versions and verified that CI passed.

Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

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

LGTM

@nzakas
Copy link
Member

nzakas commented Jun 30, 2021

@eslint/eslint-tsc if you could give this another review to make sure I didn't miss anything, I'd appreciate it. I'd like to merge this before #71.

this.visit(node.key);
}
if (node.value) {
this.scopeManager.__nestFunctionScope(node, /* strict= */ true);

Choose a reason for hiding this comment

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

how come you are nesting a function scope specifically for a property value?
or rather - why do you nest a scope at all for properties?
They can't declare variables (unless ofc the value is a function), right?

Copy link
Member Author

Choose a reason for hiding this comment

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

In the spec, class field initializers are implicit functions. Those implicit functions are called from the class instantiation process. This scope represents that straightly.

The expressions in the computed properties are evaluated one-time at defining the class. On the other hand, the expressions in the class field initializers are evaluated at each instantiating class instance. I think it's odd if those are mixed in the same scope.

They can't declare variables (unless ofc the value is a function), right?

The access to the arguments variable in the class field initializers is a syntax error, so we can't access the local variable of the implicit functions. But I'm not sure how the direct eval() behaves in the implicit functions.

Choose a reason for hiding this comment

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

Interesting!

Relevant part of the spec (I think):
In particular 3.d.

https://tc39.es/proposal-class-fields/#runtime-semantics-class-field-definition-evaluation

3. If Initializer_opt is present,
    a. Let lex be the Lexical Environment of the running execution context.
    b. Let formalParameterList be an instance of the production FormalParameters:[empty] .
    c. Let privateScope be the PrivateEnvironment of the running execution context.
    d. Let initializer be FunctionCreate(Method, formalParameterList, Initializer, lex, true, privateScope).
    e. Perform MakeMethod(initializer, homeObject).
    f. Let isAnonymousFunctionDefinition be IsAnonymousFunctionDefinition(Initializer).

If I'm understanding it correctly that means that it's essentially saying that at runtime it essentially defines a field as this:

class Foo {
  prop = 1;
}

Is more or less the same as

class Foo {
  constructor() {
    this.prop = (() => 1)();
  }
}

With the one caveat that if the intialiser contains the identifier arguments, then it's a syntax error (as defined in section 2.17.2)

Co-authored-by: Brandon Mills <btmills@users.noreply.github.com>
Comment on lines 462 to 471
PropertyDefinition(node) {
if (node.computed) {
this.visit(node.key);
}
if (node.value) {
this.scopeManager.__nestFunctionScope(node, /* strict= */ true);
this.visit(node.value);
this.close(node);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This correctly handles scope-reference relations, but it looks unusual that a node that creates a scope can contain parts that belong to its upper scope. For example, it's unexpected for context.getScope() and it will return a wrong scope when called from within a computed key:

class A {
    [x] = x;
}
"Identifier[name='x']"() {
    context.getScope().type // "function" "function";
}

Copy link
Member

Choose a reason for hiding this comment

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

@mdjermanovic what would you suggest instead? Let’s make sure to keep comments actionable so we can move things along quickly.

Copy link
Member Author

Choose a reason for hiding this comment

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

Indeed. The implicit function is the class field initializer, but not whole the class field. Probably the function scope is only the initializer node.

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 difficult to predict the impact on the existing logic in rules.

scope.block = PropertyDefinition is surprising because of the tree hierarchy.
scope.block = PropertyDefinition.value is surprising because any expression can create a scope.

If the AST spec had a function node between property and its value, that would be probably the least surprising.

I think scope.block = PropertyDefinition.value is logically correct choice.

Copy link
Member

Choose a reason for hiding this comment

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

I think scope.block = PropertyDefinition.value is logically correct choice.

Also, if we decide on this approach, would it be useful to have a different scope.type (e.g., "class-field-initializer") to make a distinction between the initializer scope and the scope that the value itself creates in some cases?

For example, if we use "function", then the ArrowFunctionExpression in the following example has two "function" scopes:

class A {
    x = () => y;
}

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 difficult to predict the impact on the existing logic in rules.

I agree.

I have introduced the class-field-initializer scope.
Though we cannot put variable declarations into the scope, the scope's variableScope is the scope itself, as the same as function scopes. Because some rules use variableScope's equality to detect being in the same function, it will be better.

Because context.getScope(node) for function nodes returns the innermost scope for the function, context.getScope(node) for function nodes never returns the new class-field-initializer scope.

Copy link
Member

Choose a reason for hiding this comment

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

Though we cannot put variable declarations into the scope, the scope's variableScope is the scope itself, as the same as function scopes. Because some rules use variableScope's equality to detect being in the same function, it will be better.

I agree, the documentation says it's about var variables, but the practical use is to find the nearest enclosing function scope. This would match the expectations of rules in eslint/eslint#14591 (comment).

Copy link
Member

Choose a reason for hiding this comment

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

Except for static fields, which seem to be a special case as they are initialized right after the class definition. From the perspective of eslint-scope I think it makes sense to treat static fields the same as instance fields, but rules will have to handle them specifically, if needed.

@nzakas
Copy link
Member

nzakas commented Jul 1, 2021

@mysticatea it looks like the last change introduced a lint error

tests/class-fields.js Outdated Show resolved Hide resolved
Copy link
Member

@mdjermanovic mdjermanovic left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

if (value) {
this.scopeManager.__nestClassFieldInitializerScope(value);
this.visit(value);
this.close(value);
Copy link
Member

Choose a reason for hiding this comment

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

Just a non-blocking observation: the class field initializer scope might have been already closed at this point, triggered by closing a child function/class scope on the same node, but then this will simply do nothing as the current scope is already set to the upper class scope.

Copy link
Member

@btmills btmills left a comment

Choose a reason for hiding this comment

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

LGTM! I like the class-field-initializer approach because it should be more apparent what's going on without needing to know the intricacies of the spec.

@nzakas nzakas merged commit 0b4a5f1 into master Jul 2, 2021
@nzakas nzakas deleted the class-fields branch July 2, 2021 18:15
@sanex3339
Copy link
Contributor

sanex3339 commented Jul 13, 2021

@nzakas Hi, Is it possible to release this before #71 ? Seems #71 takes time. Just asking)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants