-
-
Notifications
You must be signed in to change notification settings - Fork 30
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
Changes from 8 commits
ad618bc
7d0944b
2e10322
ea72415
1f1e549
ddf9af0
9239ebd
ddabe0d
a002ea2
e73b479
936ade5
c8ae315
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -434,6 +434,12 @@ class Referencer extends esrecurse.Visitor { | |
this.currentScope().__referencing(node); | ||
} | ||
|
||
// eslint-disable-next-line class-methods-use-this | ||
PrivateIdentifier() { | ||
|
||
// Do nothing. | ||
} | ||
|
||
UpdateExpression(node) { | ||
if (PatternVisitor.isPattern(node.argument)) { | ||
this.currentScope().__referencing(node.argument, Reference.RW, null); | ||
|
@@ -453,6 +459,17 @@ class Referencer extends esrecurse.Visitor { | |
this.visitProperty(node); | ||
} | ||
|
||
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); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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";
} There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
If the AST spec had a function node between property and its value, that would be probably the least surprising. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Also, if we decide on this approach, would it be useful to have a different For example, if we use class A {
x = () => y;
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree. I have introduced the Because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree, the documentation says it's about There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
MethodDefinition(node) { | ||
this.visitProperty(node); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,290 @@ | ||
/** | ||
* @fileoverview Tests for class fields syntax. | ||
* @author Toru Nagashima | ||
*/ | ||
|
||
"use strict"; | ||
nzakas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const assert = require("assert"); | ||
const espree = require("espree"); | ||
const { KEYS } = require("eslint-visitor-keys"); | ||
const { analyze } = require("../lib/index"); | ||
|
||
describe("Class fields", () => { | ||
describe("class C { f = g }", () => { | ||
let scopes; | ||
|
||
beforeEach(() => { | ||
const ast = espree.parse("class C { f = g }", { ecmaVersion: 13 }); | ||
const manager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); | ||
|
||
scopes = manager.globalScope.childScopes; | ||
}); | ||
|
||
it("should create a class scope.", () => { | ||
assert.strictEqual(scopes.length, 1); | ||
assert.strictEqual(scopes[0].type, "class"); | ||
}); | ||
|
||
it("The class scope has no references.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.references.length, 0); | ||
}); | ||
|
||
it("The class scope has only the variable 'C'; it doesn't have the field name 'f'.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.variables.length, 1); | ||
assert.strictEqual(classScope.variables[0].name, "C"); | ||
}); | ||
|
||
it("The class scope has a function scope.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.childScopes.length, 1); | ||
assert.strictEqual(classScope.childScopes[0].type, "function"); | ||
}); | ||
|
||
it("The function scope has only the reference 'g'.", () => { | ||
const classScope = scopes[0]; | ||
const fieldInitializerScope = classScope.childScopes[0]; | ||
|
||
assert.strictEqual(fieldInitializerScope.references.length, 1); | ||
assert.strictEqual(fieldInitializerScope.references[0].identifier.name, "g"); | ||
}); | ||
|
||
it("The function scope has no variables.", () => { | ||
const classScope = scopes[0]; | ||
const fieldInitializerScope = classScope.childScopes[0]; | ||
|
||
assert.strictEqual(fieldInitializerScope.variables.length, 0); | ||
}); | ||
}); | ||
|
||
describe("class C { f }", () => { | ||
let scopes; | ||
|
||
beforeEach(() => { | ||
const ast = espree.parse("class C { f }", { ecmaVersion: 13 }); | ||
const manager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); | ||
|
||
scopes = manager.globalScope.childScopes; | ||
}); | ||
|
||
it("should create a class scope.", () => { | ||
assert.strictEqual(scopes.length, 1); | ||
assert.strictEqual(scopes[0].type, "class"); | ||
}); | ||
|
||
it("The class scope has no references.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.references.length, 0); | ||
}); | ||
|
||
it("The class scope has no child scopes; fields that don't have initializers don't create any function scopes.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.childScopes.length, 0); | ||
}); | ||
|
||
it("The class scope has only the variable 'C'; it doesn't have the field name 'f'.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.variables.length, 1); | ||
assert.strictEqual(classScope.variables[0].name, "C"); | ||
}); | ||
}); | ||
|
||
describe("class C { #f = g }", () => { | ||
let scopes; | ||
|
||
beforeEach(() => { | ||
const ast = espree.parse("class C { #f = g }", { ecmaVersion: 13 }); | ||
const manager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); | ||
|
||
scopes = manager.globalScope.childScopes; | ||
}); | ||
|
||
it("should create a class scope.", () => { | ||
assert.strictEqual(scopes.length, 1); | ||
assert.strictEqual(scopes[0].type, "class"); | ||
}); | ||
|
||
it("The class scope has no references.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.references.length, 0); | ||
}); | ||
|
||
it("The class scope has only the variable 'C'; it doesn't have the field name '#f'.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.variables.length, 1); | ||
assert.strictEqual(classScope.variables[0].name, "C"); | ||
}); | ||
|
||
it("The class scope has a function scope.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.childScopes.length, 1); | ||
assert.strictEqual(classScope.childScopes[0].type, "function"); | ||
}); | ||
|
||
it("The function scope has only the reference 'g'.", () => { | ||
const classScope = scopes[0]; | ||
const fieldInitializerScope = classScope.childScopes[0]; | ||
|
||
assert.strictEqual(fieldInitializerScope.references.length, 1); | ||
assert.strictEqual(fieldInitializerScope.references[0].identifier.name, "g"); | ||
}); | ||
|
||
it("The function scope has no variables.", () => { | ||
const classScope = scopes[0]; | ||
const fieldInitializerScope = classScope.childScopes[0]; | ||
|
||
assert.strictEqual(fieldInitializerScope.variables.length, 0); | ||
}); | ||
}); | ||
|
||
describe("class C { [fname] }", () => { | ||
let scopes; | ||
|
||
beforeEach(() => { | ||
const ast = espree.parse("class C { [fname] }", { ecmaVersion: 13 }); | ||
const manager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); | ||
|
||
scopes = manager.globalScope.childScopes; | ||
}); | ||
|
||
it("should create a class scope.", () => { | ||
assert.strictEqual(scopes.length, 1); | ||
assert.strictEqual(scopes[0].type, "class"); | ||
}); | ||
|
||
it("The class scope has only the reference 'fname'.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.references.length, 1); | ||
assert.strictEqual(classScope.references[0].identifier.name, "fname"); | ||
}); | ||
|
||
it("The class scope has no child scopes; fields that don't have initializers don't create any function scopes.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.childScopes.length, 0); | ||
}); | ||
}); | ||
|
||
describe("class C { [fname] = value }", () => { | ||
let scopes; | ||
|
||
beforeEach(() => { | ||
const ast = espree.parse("class C { [fname] = value }", { ecmaVersion: 13 }); | ||
const manager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); | ||
|
||
scopes = manager.globalScope.childScopes; | ||
}); | ||
|
||
it("should create a class scope.", () => { | ||
assert.strictEqual(scopes.length, 1); | ||
assert.strictEqual(scopes[0].type, "class"); | ||
}); | ||
|
||
it("The class scope has only the reference 'fname'; it doesn't have the reference 'value'.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.references.length, 1); | ||
assert.strictEqual(classScope.references[0].identifier.name, "fname"); | ||
}); | ||
|
||
it("The class scope has a function scope.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.childScopes.length, 1); | ||
assert.strictEqual(classScope.childScopes[0].type, "function"); | ||
}); | ||
|
||
it("The function scope has the reference 'value'.", () => { | ||
const classScope = scopes[0]; | ||
const fieldInitializerScope = classScope.childScopes[0]; | ||
|
||
assert.strictEqual(fieldInitializerScope.references.length, 1); | ||
assert.strictEqual(fieldInitializerScope.references[0].identifier.name, "value"); | ||
}); | ||
|
||
it("The function scope has no variables.", () => { | ||
const classScope = scopes[0]; | ||
const fieldInitializerScope = classScope.childScopes[0]; | ||
|
||
assert.strictEqual(fieldInitializerScope.variables.length, 0); | ||
}); | ||
}); | ||
mysticatea marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
btmills marked this conversation as resolved.
Show resolved
Hide resolved
|
||
describe("class C { #f = g; e = this.#f }", () => { | ||
let scopes; | ||
|
||
beforeEach(() => { | ||
const ast = espree.parse("class C { #f = g; e = this.#f }", { ecmaVersion: 13 }); | ||
const manager = analyze(ast, { ecmaVersion: 13, childVistorKeys: KEYS }); | ||
|
||
scopes = manager.globalScope.childScopes; | ||
}); | ||
|
||
it("should create a class scope.", () => { | ||
assert.strictEqual(scopes.length, 1); | ||
assert.strictEqual(scopes[0].type, "class"); | ||
}); | ||
|
||
it("The class scope has no references.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.references.length, 0); | ||
}); | ||
|
||
it("The class scope has only the variable 'C'; it doesn't have the field names '#f' or 'e'.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.variables.length, 1); | ||
assert.strictEqual(classScope.variables[0].name, "C"); | ||
}); | ||
|
||
it("The class scope has two function scopes.", () => { | ||
const classScope = scopes[0]; | ||
|
||
assert.strictEqual(classScope.childScopes.length, 2); | ||
assert.strictEqual(classScope.childScopes[0].type, "function"); | ||
assert.strictEqual(classScope.childScopes[1].type, "function"); | ||
}); | ||
|
||
it("The first function scope has only the reference 'g'.", () => { | ||
const classScope = scopes[0]; | ||
const fieldInitializerScope = classScope.childScopes[0]; | ||
|
||
assert.strictEqual(fieldInitializerScope.references.length, 1); | ||
assert.strictEqual(fieldInitializerScope.references[0].identifier.name, "g"); | ||
}); | ||
|
||
it("The first function scope has no variables.", () => { | ||
const classScope = scopes[0]; | ||
const fieldInitializerScope = classScope.childScopes[0]; | ||
|
||
assert.strictEqual(fieldInitializerScope.variables.length, 0); | ||
}); | ||
|
||
it("The second function scope has no references.", () => { | ||
const classScope = scopes[0]; | ||
const fieldInitializerScope = classScope.childScopes[1]; | ||
|
||
assert.strictEqual(fieldInitializerScope.references.length, 0); | ||
}); | ||
|
||
it("The second function scope has no variables.", () => { | ||
const classScope = scopes[0]; | ||
const fieldInitializerScope = classScope.childScopes[1]; | ||
|
||
assert.strictEqual(fieldInitializerScope.variables.length, 0); | ||
}); | ||
}); | ||
}); |
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.
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?
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.
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.
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 directeval()
behaves in the implicit functions.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.
Interesting!
Relevant part of the spec (I think):
In particular 3.d.
https://tc39.es/proposal-class-fields/#runtime-semantics-class-field-definition-evaluation
If I'm understanding it correctly that means that it's essentially saying that at runtime it essentially defines a field as this:
Is more or less the same as
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)