diff --git a/lib/referencer.js b/lib/referencer.js index 63d1935..7a79118 100644 --- a/lib/referencer.js +++ b/lib/referencer.js @@ -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,19 @@ class Referencer extends esrecurse.Visitor { this.visitProperty(node); } + PropertyDefinition(node) { + const { computed, key, value } = node; + + if (computed) { + this.visit(key); + } + if (value) { + this.scopeManager.__nestClassFieldInitializerScope(value); + this.visit(value); + this.close(value); + } + } + MethodDefinition(node) { this.visitProperty(node); } diff --git a/lib/scope-manager.js b/lib/scope-manager.js index c192799..ef56170 100644 --- a/lib/scope-manager.js +++ b/lib/scope-manager.js @@ -25,20 +25,21 @@ /* eslint-disable no-underscore-dangle */ -const Scope = require("./scope"); +const { + BlockScope, + CatchScope, + ClassFieldInitializerScope, + ClassScope, + ForScope, + FunctionExpressionNameScope, + FunctionScope, + GlobalScope, + ModuleScope, + SwitchScope, + WithScope +} = require("./scope"); const assert = require("assert"); -const GlobalScope = Scope.GlobalScope; -const CatchScope = Scope.CatchScope; -const WithScope = Scope.WithScope; -const ModuleScope = Scope.ModuleScope; -const ClassScope = Scope.ClassScope; -const SwitchScope = Scope.SwitchScope; -const FunctionScope = Scope.FunctionScope; -const ForScope = Scope.ForScope; -const FunctionExpressionNameScope = Scope.FunctionExpressionNameScope; -const BlockScope = Scope.BlockScope; - /** * @class ScopeManager */ @@ -225,6 +226,10 @@ class ScopeManager { return this.__nestScope(new ClassScope(this, this.__currentScope, node)); } + __nestClassFieldInitializerScope(node) { + return this.__nestScope(new ClassFieldInitializerScope(this, this.__currentScope, node)); + } + __nestSwitchScope(node) { return this.__nestScope(new SwitchScope(this, this.__currentScope, node)); } diff --git a/lib/scope.js b/lib/scope.js index bdb5f63..651f56a 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -224,7 +224,7 @@ class Scope { * @member {Scope} Scope#variableScope */ this.variableScope = - (this.type === "global" || this.type === "function" || this.type === "module") ? this : upperScope.variableScope; + (this.type === "global" || this.type === "function" || this.type === "module" || this.type === "class-field-initializer") ? this : upperScope.variableScope; /** * Whether this scope is created by a FunctionExpression. @@ -731,6 +731,12 @@ class ClassScope extends Scope { } } +class ClassFieldInitializerScope extends Scope { + constructor(scopeManager, upperScope, block) { + super(scopeManager, "class-field-initializer", upperScope, block, true); + } +} + module.exports = { Scope, GlobalScope, @@ -742,7 +748,8 @@ module.exports = { SwitchScope, FunctionScope, ForScope, - ClassScope + ClassScope, + ClassFieldInitializerScope }; /* vim: set sw=4 ts=4 et tw=80 : */ diff --git a/package.json b/package.json index f899faf..b1739df 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "eslint-config-eslint": "^5.0.1", "eslint-plugin-node": "^9.1.0", "eslint-release": "^1.0.0", - "eslint-visitor-keys": "^1.2.0", - "espree": "^7.1.0", + "eslint-visitor-keys": "^3.0.0", + "espree": "^8.0.0", "istanbul": "^0.4.5", "mocha": "^6.1.4", "npm-license": "^0.3.3", diff --git a/tests/class-fields.js b/tests/class-fields.js new file mode 100644 index 0000000..a8739aa --- /dev/null +++ b/tests/class-fields.js @@ -0,0 +1,305 @@ +/** + * @fileoverview Tests for class fields syntax. + * @author Toru Nagashima + */ + +"use strict"; + +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 class-field-initializer scope.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-field-initializer"); + }); + + it("The class-field-initializer scope's block is the node of the field initializer.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.block.type, "Identifier"); + assert.strictEqual(fieldInitializerScope.block.name, "g"); + }); + + it("The class-field-initializer scope's variableScope is itself.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.variableScope, fieldInitializerScope); + }); + + it("The class-field-initializer 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 class-field-initializer 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 class-field-initializer 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 class-field-initializer scope.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-field-initializer"); + }); + + it("The class-field-initializer 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 class-field-initializer 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 class-field-initializer 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 class-field-initializer scope.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-field-initializer"); + }); + + it("The class-field-initializer 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 class-field-initializer scope has no variables.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.variables.length, 0); + }); + }); + + 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 class-field-initializer scopes.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.childScopes.length, 2); + assert.strictEqual(classScope.childScopes[0].type, "class-field-initializer"); + assert.strictEqual(classScope.childScopes[1].type, "class-field-initializer"); + }); + + it("The first class-field-initializer 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 class-field-initializer scope has no variables.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.variables.length, 0); + }); + + it("The second class-field-initializer scope has no references.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[1]; + + assert.strictEqual(fieldInitializerScope.references.length, 0); + }); + + it("The second class-field-initializer scope has no variables.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[1]; + + assert.strictEqual(fieldInitializerScope.variables.length, 0); + }); + }); +});