From d2096bcdd56acb529e6fe73ae85674c23583967e Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 9 Nov 2021 22:38:35 +0100 Subject: [PATCH] feat!: support class static blocks --- lib/referencer.js | 8 + lib/scope-manager.js | 5 + lib/scope.js | 20 +- package.json | 4 +- tests/class-static-blocks.js | 875 +++++++++++++++++++++++++++++++++++ 5 files changed, 907 insertions(+), 5 deletions(-) create mode 100644 tests/class-static-blocks.js diff --git a/lib/referencer.js b/lib/referencer.js index 0b90ec8..0147f68 100644 --- a/lib/referencer.js +++ b/lib/referencer.js @@ -470,6 +470,14 @@ class Referencer extends esrecurse.Visitor { } } + StaticBlock(node) { + this.scopeManager.__nestClassStaticBlockScope(node); + + this.visitChildren(node); + + this.close(node); + } + MethodDefinition(node) { this.visitProperty(node); } diff --git a/lib/scope-manager.js b/lib/scope-manager.js index 5a12314..c6058f7 100644 --- a/lib/scope-manager.js +++ b/lib/scope-manager.js @@ -28,6 +28,7 @@ import { BlockScope, CatchScope, ClassFieldInitializerScope, + ClassStaticBlockScope, ClassScope, ForScope, FunctionExpressionNameScope, @@ -228,6 +229,10 @@ class ScopeManager { return this.__nestScope(new ClassFieldInitializerScope(this, this.__currentScope, node)); } + __nestClassStaticBlockScope(node) { + return this.__nestScope(new ClassStaticBlockScope(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 73386e6..42c0693 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -157,7 +157,8 @@ class Scope { constructor(scopeManager, type, upperScope, block, isMethodDefinition) { /** - * One of 'module', 'block', 'switch', 'function', 'catch', 'with', 'function', 'class', 'global'. + * One of "global", "module", "function", "function-expression-name", "block", "switch", "catch", "with", "for", + * "class", "class-field-initializer", "class-static-block". * @member {string} Scope#type */ this.type = type; @@ -225,7 +226,13 @@ class Scope { * @member {Scope} Scope#variableScope */ this.variableScope = - (this.type === "global" || this.type === "function" || this.type === "module" || this.type === "class-field-initializer") ? this : upperScope.variableScope; + this.type === "global" || + this.type === "module" || + this.type === "function" || + this.type === "class-field-initializer" || + this.type === "class-static-block" + ? this + : upperScope.variableScope; /** * Whether this scope is created by a FunctionExpression. @@ -738,6 +745,12 @@ class ClassFieldInitializerScope extends Scope { } } +class ClassStaticBlockScope extends Scope { + constructor(scopeManager, upperScope, block) { + super(scopeManager, "class-static-block", upperScope, block, true); + } +} + export { Scope, GlobalScope, @@ -750,7 +763,8 @@ export { FunctionScope, ForScope, ClassScope, - ClassFieldInitializerScope + ClassFieldInitializerScope, + ClassStaticBlockScope }; /* vim: set sw=4 ts=4 et tw=80 : */ diff --git a/package.json b/package.json index 0e2098b..63d8e3c 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,8 @@ "eslint-plugin-jsdoc": "^35.4.1", "eslint-plugin-node": "^11.1.0", "eslint-release": "^3.2.0", - "eslint-visitor-keys": "^3.0.0", - "espree": "^8.0.0", + "eslint-visitor-keys": "^3.1.0", + "espree": "^9.0.0", "mocha": "^9.0.1", "npm-license": "^0.3.3", "rollup": "^2.52.7", diff --git a/tests/class-static-blocks.js b/tests/class-static-blocks.js new file mode 100644 index 0000000..c16e23e --- /dev/null +++ b/tests/class-static-blocks.js @@ -0,0 +1,875 @@ +/** + * @fileoverview Tests for class static blocks. + * @author Milos Djermanovic + */ + +import assert from "assert"; +import * as espree from "espree"; +import { KEYS } from "eslint-visitor-keys"; +import { analyze } from "../lib/index.js"; + +describe("Class static blocks", () => { + + describe("class C { static { var a; let b; const c = 1; function d(){} class e {} } }", () => { + let ast; + let scopeManager; + let globalScope; + + beforeEach(() => { + ast = espree.parse("class C { static { var a; let b; const c = 1; function d(){} class e {} } }", { ecmaVersion: 13 }); + scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + ({ globalScope } = scopeManager); + }); + + it("the global scope should have one variable, named `C`", () => { + assert.strictEqual(globalScope.variables.length, 1); + assert.strictEqual(globalScope.variables[0].name, "C"); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have one variable, named `C`", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.variables.length, 1); + assert.strictEqual(classScope.variables[0].name, "C"); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have two child scopes, a function and a class scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.childScopes.length, 2); + assert.strictEqual(classStaticBlockScope.childScopes[0].type, "function"); + assert.strictEqual(classStaticBlockScope.childScopes[0].upper, classStaticBlockScope); + assert.strictEqual(classStaticBlockScope.childScopes[1].type, "class"); + assert.strictEqual(classStaticBlockScope.childScopes[1].upper, classStaticBlockScope); + }); + + it("the class static block scope's `upper` scope should be the class scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.upper, classScope); + }); + + it("the class static block scope's `variableScope` is itself", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.variableScope, classStaticBlockScope); + }); + + it("the class static block scope should be strict", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.isStrict, true); + }); + + it("the class static block scope's `functionExpressionScope` property should have value `false`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.functionExpressionScope, false); + }); + + it("the class static block scope's `block` node is the `StaticBlock` node from the AST", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const staticBlockNode = ast.body[0].body.body[0]; + + assert.strictEqual(staticBlockNode.type, "StaticBlock"); + assert.strictEqual(classStaticBlockScope.block, staticBlockNode); + }); + + it("the class static block scope should be returned by ScopeManager#acquire for the `StaticBlock` node from the AST", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const staticBlockNode = ast.body[0].body.body[0]; + + assert.strictEqual(staticBlockNode.type, "StaticBlock"); + assert.strictEqual(scopeManager.acquire(staticBlockNode, /* inner = */ false), classStaticBlockScope); + assert.strictEqual(scopeManager.acquire(staticBlockNode, /* inner = */ true), classStaticBlockScope); + }); + + it("the `StaticBlock` node itself doesn't declare any variables", () => { + const staticBlockNode = ast.body[0].body.body[0]; + + assert.strictEqual(staticBlockNode.type, "StaticBlock"); + assert.strictEqual(scopeManager.getDeclaredVariables(staticBlockNode).length, 0); + }); + + it("the class static block scope should have 5 variables: `a`, `b`, `c`, `d`, `e`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const expectedVariableNames = ["a", "b", "c", "d", "e"]; + + assert.deepStrictEqual(classStaticBlockScope.variables.map(v => v.name), expectedVariableNames); + assert.deepStrictEqual([...classStaticBlockScope.set.keys()], expectedVariableNames); + assert.deepStrictEqual([...classStaticBlockScope.set.values()], classStaticBlockScope.variables); + classStaticBlockScope.variables.forEach(variable => { + assert.strictEqual(variable.scope, classStaticBlockScope); + }); + }); + }); + + describe("class C { static { function f(){} f(); } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("class C { static { function f(){} f(); } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have no references", () => { + assert.strictEqual(globalScope.references.length, 0); + }); + + it("the global scope should have no `through` references", () => { + assert.strictEqual(globalScope.through.length, 0); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have no `through` references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 0); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have no `through` references`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.through.length, 0); + }); + + it("the class static block scope should have one variable, named `f`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.variables.length, 1); + assert.strictEqual(classStaticBlockScope.variables[0].name, "f"); + }); + + it("the class static block scope should have one reference, to the variable `f`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.references.length, 1); + assert.strictEqual(classStaticBlockScope.references[0].resolved, classStaticBlockScope.variables[0]); + }); + + it("the variable `f` should have one reference, from the class static block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const f = classStaticBlockScope.variables[0]; + + assert.strictEqual(f.references.length, 1); + assert.strictEqual(f.references[0].from, classStaticBlockScope); + assert.strictEqual(f.references[0], classStaticBlockScope.references[0]); + }); + }); + + describe("class C { static { a = 1; if (this.x) { var a; } } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("class C { static { a = 1; if (this.x) { var a; } } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have no references", () => { + assert.strictEqual(globalScope.references.length, 0); + }); + + it("the global scope should have no `through` references", () => { + assert.strictEqual(globalScope.through.length, 0); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have no `through` references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 0); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have no `through` references`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.through.length, 0); + }); + + it("the class static block scope should have one variable, named `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.variables.length, 1); + assert.strictEqual(classStaticBlockScope.variables[0].name, "a"); + }); + + it("the class static block scope should have one reference, to the variable `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.references.length, 1); + assert.strictEqual(classStaticBlockScope.references[0].resolved, classStaticBlockScope.variables[0]); + }); + + it("the variable `a` should have one reference, from the class static block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const a = classStaticBlockScope.variables[0]; + + assert.strictEqual(a.references.length, 1); + assert.strictEqual(a.references[0].from, classStaticBlockScope); + }); + + it("the class static block scope should have one child scope, a block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.childScopes.length, 1); + assert.strictEqual(classStaticBlockScope.childScopes[0].type, "block"); + }); + + it("the block scope should have no variables", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + assert.strictEqual(blockScope.variables.length, 0); + }); + + it("the block scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + assert.strictEqual(blockScope.references.length, 0); + }); + }); + + describe("class C { static { if (this.x) { var a; a = 1; } } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("class C { static { if (this.x) { var a; a = 1; } } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have no references", () => { + assert.strictEqual(globalScope.references.length, 0); + }); + + it("the global scope should have no `through` references", () => { + assert.strictEqual(globalScope.through.length, 0); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have no `through` references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 0); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have no `through` references`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.through.length, 0); + }); + + it("the class static block scope should have one variable, named `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.variables.length, 1); + assert.strictEqual(classStaticBlockScope.variables[0].name, "a"); + }); + + it("the class static block scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.references.length, 0); + }); + + it("the class static block scope should have one child scope, a block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.childScopes.length, 1); + assert.strictEqual(classStaticBlockScope.childScopes[0].type, "block"); + }); + + it("the variable `a` should have one reference, from the block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + const a = classStaticBlockScope.variables[0]; + + assert.strictEqual(a.references.length, 1); + assert.strictEqual(a.references[0].from, blockScope); + }); + + it("the block scope should have no variables", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + assert.strictEqual(blockScope.variables.length, 0); + }); + + it("the block scope should have one reference, to the variable `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + const a = classStaticBlockScope.variables[0]; + + assert.strictEqual(blockScope.references.length, 1); + assert.strictEqual(blockScope.references[0].resolved, a); + }); + }); + + describe("class C { static { const { a } = this.foo; if (this.bar) { const b = a + 1; this.baz(b); } } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("class C { static { const { a } = this.foo; if (this.bar) { const b = a + 1; this.baz(b); } } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have no references", () => { + assert.strictEqual(globalScope.references.length, 0); + }); + + it("the global scope should have no `through` references", () => { + assert.strictEqual(globalScope.through.length, 0); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have no `through` references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 0); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have one child scope, a block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.childScopes.length, 1); + assert.strictEqual(classStaticBlockScope.childScopes[0].type, "block"); + }); + + it("the class static block scope should have one variable, named `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.variables.length, 1); + assert.strictEqual(classStaticBlockScope.variables[0].name, "a"); + }); + + it("the variable `a` should have a write reference from the class static block scope, and a read reference from the block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + const a = classStaticBlockScope.variables[0]; + + assert.strictEqual(a.references.length, 2); + assert.strictEqual(a.references[0].isWriteOnly(), true); + assert.strictEqual(a.references[0].from, classStaticBlockScope); + assert.strictEqual(a.references[1].isReadOnly(), true); + assert.strictEqual(a.references[1].from, blockScope); + }); + + it("the class static block scope should have no `through` references`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.through.length, 0); + }); + + it("the block scope should have one variable, named `b`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + assert.strictEqual(blockScope.variables.length, 1); + assert.strictEqual(blockScope.variables[0].name, "b"); + }); + + it("the variable `b` should have a write reference and a read reference from the block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + const b = blockScope.variables[0]; + + assert.strictEqual(b.references.length, 2); + assert.strictEqual(b.references[0].isWriteOnly(), true); + assert.strictEqual(b.references[0].from, blockScope); + assert.strictEqual(b.references[1].isReadOnly(), true); + assert.strictEqual(b.references[1].from, blockScope); + }); + + it("the block scope should have one `through` reference, to the variable `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + const a = classStaticBlockScope.variables[0]; + + assert.strictEqual(blockScope.through.length, 1); + assert.strictEqual(blockScope.through[0].resolved, a); + }); + }); + + describe("class C { static { C.x; } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("class C { static { C.x; } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have no references", () => { + assert.strictEqual(globalScope.references.length, 0); + }); + + it("the global scope should have no `through` references", () => { + assert.strictEqual(globalScope.through.length, 0); + }); + + it("the global scope should have variable `C`", () => { + assert.strictEqual(globalScope.set.has("C"), true); + }); + + it("the global variable `C` should have no references", () => { + assert.strictEqual(globalScope.set.get("C").references.length, 0); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have no `through` references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 0); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class scope should have variable `C`", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.set.has("C"), true); + }); + + it("the class scope's variable `C` should have one reference, from the class static block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + const C = classScope.set.get("C"); + + assert.strictEqual(C.references.length, 1); + assert.strictEqual(C.references[0].from, classStaticBlockScope); + }); + + it("the class static block scope should have one reference, to the class scope's variable `C`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + const C = classScope.set.get("C"); + + assert.strictEqual(classStaticBlockScope.references.length, 1); + assert.strictEqual(classStaticBlockScope.references[0].resolved, C); + }); + + it("the class static block scope should have one `through` reference, to the class scope's variable `C`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + const C = classScope.set.get("C"); + + assert.strictEqual(classStaticBlockScope.through.length, 1); + assert.strictEqual(classStaticBlockScope.through[0].resolved, C); + }); + + it("the class static block scope should have no variables", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.variables.length, 0); + }); + }); + + describe("let a; class C { static { lbl: { this.b = a } } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("let a; class C { static { lbl: { this.b = a } } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have no references", () => { + assert.strictEqual(globalScope.references.length, 0); + }); + + it("the global scope should have no `through` references", () => { + assert.strictEqual(globalScope.through.length, 0); + }); + + it("the global scope should have variable `a`", () => { + assert.strictEqual(globalScope.set.has("a"), true); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have one `through` reference, a reference to `a`", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 1); + assert.strictEqual(classScope.through[0].resolved, globalScope.set.get("a")); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.references.length, 0); + }); + + it("the class static block scope should have one `through` reference, a reference to `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.through.length, 1); + assert.strictEqual(classStaticBlockScope.through[0].resolved, globalScope.set.get("a")); + }); + + it("the class static block scope should have one child scope, a block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.childScopes.length, 1); + assert.strictEqual(classStaticBlockScope.childScopes[0].type, "block"); + }); + + it("the block scope should have one reference, a reference to `a", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + assert.strictEqual(blockScope.references.length, 1); + assert.strictEqual(blockScope.references[0].resolved, globalScope.set.get("a")); + }); + + it("the block scope should have one `through` reference, a reference to `a", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + assert.strictEqual(blockScope.through.length, 1); + assert.strictEqual(blockScope.through[0].resolved, globalScope.set.get("a")); + }); + }); + + describe("class C { static { a; } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("class C { static { a; } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have one unresolved reference, with identifier `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.references.length, 1); + assert.strictEqual(classStaticBlockScope.references[0].resolved, null); + }); + + it("the class static block scope and all upper scopes should have the unresolved reference in `through`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + const reference = classStaticBlockScope.references[0]; + + let scope = classStaticBlockScope; + + do { + assert.strictEqual(scope.through[0], reference); + scope = scope.upper; + } while (scope); + }); + }); + + describe("let a; class C { static { let a; a; } static { a; let a; } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("let a; class C { static { let a; a; } static { a; let a; } }", { ecmaVersion: 13, range: true }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have variable `a`", () => { + assert.strictEqual(globalScope.set.has("a"), true); + }); + + it("the global scope variable `a` should not have any references", () => { + assert.strictEqual(globalScope.set.get("a").references.length, 0); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have no `through` references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 0); + }); + + it("the class scope should have two child scopes, and those are two different class static block scopes", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 2); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + assert.strictEqual(classScope.childScopes[0].block.type, "StaticBlock"); + assert.strictEqual(classScope.childScopes[1].type, "class-static-block"); + assert.strictEqual(classScope.childScopes[1].block.type, "StaticBlock"); + assert.ok(classScope.childScopes[0] !== classScope.childScopes[1], "Class static block scopes are same"); + assert.ok(classScope.childScopes[0].block !== classScope.childScopes[1].block, "Class static block scopes are on the same node"); + assert.ok(classScope.childScopes[0].upper === classScope.childScopes[1].upper, "Class static block scopes don't have the same upper scope"); + }); + + it("each of the two class static block scopes should have one variable `a` and one reference to that variable", () => { + const classScope = globalScope.childScopes[0]; + + classScope.childScopes.forEach(classStaticBlockScope => { + assert.strictEqual(classStaticBlockScope.variables.length, 1); + + const variable = classStaticBlockScope.variables[0]; + + assert.ok(variable.scope === classStaticBlockScope, "Variable is from another scope"); + assert.strictEqual(variable.name, "a"); + assert.strictEqual(variable.references.length, 1); + + const reference = variable.references[0]; + const referenceIdentifier = reference.identifier; + const staticBlockNode = classStaticBlockScope.block; + + assert.ok( + reference.from === classStaticBlockScope, + "Reference is from another scope" + ); + + assert.ok( + staticBlockNode.range[0] <= referenceIdentifier.range[0] && + referenceIdentifier.range[1] <= staticBlockNode.range[1], + "Reference is from another node" + ); + }); + }); + }); + + describe("let a; class C { [a]; static { let a; } [a]; static { function a(){} } [a]; static { var a; } [a]; }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("let a; class C { [a]; static { let a; } [a]; static { function a(){} } [a]; static { var a; } [a]; }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have variable `a`", () => { + assert.strictEqual(globalScope.set.has("a"), true); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the global variable `a` should have 4 references from the class scope", () => { + const a = globalScope.set.get("a"); + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(a.references.length, 4); + + a.references.forEach(reference => { + assert.strictEqual(reference.from, classScope); + }); + }); + + it("the class scope should have 3 child scopes, and those are class static block scopes", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 3); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + assert.strictEqual(classScope.childScopes[1].type, "class-static-block"); + assert.strictEqual(classScope.childScopes[2].type, "class-static-block"); + }); + + it("each of the class static block scopes should have one variable named `a` with no references", () => { + const classScope = globalScope.childScopes[0]; + + classScope.childScopes.forEach(classStaticBlockScope => { + assert.strictEqual(classStaticBlockScope.variables.length, 1); + + const variable = classStaticBlockScope.variables[0]; + + assert.strictEqual(variable.name, "a"); + assert.strictEqual(variable.references.length, 0); + }); + }); + }); +});