Skip to content
This repository has been archived by the owner on Aug 15, 2024. It is now read-only.

Commit

Permalink
fix!: class extends is evaluated in the class scope (#116)
Browse files Browse the repository at this point in the history
* fix!: class `extends` is evaluated in the class scope

Fixes #59

* add tests with nested scopes
  • Loading branch information
mdjermanovic authored Jan 2, 2024
1 parent ed67857 commit 42ef7a9
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 14 deletions.
4 changes: 2 additions & 2 deletions lib/referencer.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,6 @@ class Referencer extends esrecurse.Visitor {
));
}

this.visit(node.superClass);

this.scopeManager.__nestClassScope(node);

if (node.id) {
Expand All @@ -284,6 +282,8 @@ class Referencer extends esrecurse.Visitor {
node
));
}

this.visit(node.superClass);
this.visit(node.body);

this.close(node);
Expand Down
206 changes: 197 additions & 9 deletions tests/es6-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ describe("ES6 class", () => {
expect(scope.isStrict).to.be.false;
expect(scope.variables).to.have.length(1);
expect(scope.variables[0].name).to.be.equal("Derived");
expect(scope.references).to.have.length(2);
expect(scope.references[0].identifier.name).to.be.equal("Base");
expect(scope.references[1].identifier.name).to.be.equal("Derived");
expect(scope.references).to.have.length(1);
expect(scope.references[0].identifier.name).to.be.equal("Derived");

scope = scopeManager.scopes[1];
expect(scope.type).to.be.equal("class");
expect(scope.block.type).to.be.equal("ClassDeclaration");
expect(scope.isStrict).to.be.true;
expect(scope.variables).to.have.length(1);
expect(scope.variables[0].name).to.be.equal("Derived");
expect(scope.references).to.have.length(0);
expect(scope.references).to.have.length(1);
expect(scope.references[0].identifier.name).to.be.equal("Base");

scope = scopeManager.scopes[2];
expect(scope.type).to.be.equal("function");
Expand Down Expand Up @@ -87,16 +87,16 @@ describe("ES6 class", () => {
expect(scope.block.type).to.be.equal("Program");
expect(scope.isStrict).to.be.false;
expect(scope.variables).to.have.length(0);
expect(scope.references).to.have.length(1);
expect(scope.references[0].identifier.name).to.be.equal("Base");
expect(scope.references).to.have.length(0);

scope = scopeManager.scopes[1];
expect(scope.type).to.be.equal("class");
expect(scope.block.type).to.be.equal("ClassExpression");
expect(scope.isStrict).to.be.true;
expect(scope.variables).to.have.length(1);
expect(scope.variables[0].name).to.be.equal("Derived");
expect(scope.references).to.have.length(0);
expect(scope.references).to.have.length(1);
expect(scope.references[0].identifier.name).to.be.equal("Base");

scope = scopeManager.scopes[2];
expect(scope.type).to.be.equal("function");
Expand All @@ -121,12 +121,13 @@ describe("ES6 class", () => {
expect(scope.block.type).to.be.equal("Program");
expect(scope.isStrict).to.be.false;
expect(scope.variables).to.have.length(0);
expect(scope.references).to.have.length(1);
expect(scope.references[0].identifier.name).to.be.equal("Base");
expect(scope.references).to.have.length(0);

scope = scopeManager.scopes[1];
expect(scope.type).to.be.equal("class");
expect(scope.block.type).to.be.equal("ClassExpression");
expect(scope.references).to.have.length(1);
expect(scope.references[0].identifier.name).to.be.equal("Base");

scope = scopeManager.scopes[2];
expect(scope.type).to.be.equal("function");
Expand Down Expand Up @@ -177,6 +178,193 @@ describe("ES6 class", () => {
expect(scope.references[1].identifier.name).to.be.equal("yuyushiki");
});

// https://github.com/eslint/eslint-scope/issues/59
it("class heritage may refer class name in class expressions #1", () => {
const ast = espree(`
const A = class A extends A {}
`);

const scopeManager = analyze(ast, { ecmaVersion: 6 });

expect(scopeManager.scopes).to.have.length(2);

let scope = scopeManager.scopes[0];

expect(scope.type).to.be.equal("global");
expect(scope.block.type).to.be.equal("Program");
expect(scope.isStrict).to.be.false;
expect(scope.variables).to.have.length(1);
expect(scope.variables[0].name).to.be.equal("A"); // variable `A` defined by `const A`
expect(scope.variables[0].references).to.have.length(1); // init reference `A` in `const A`
expect(scope.variables[0].references[0].init).to.be.true;
expect(scope.references).to.have.length(1);
expect(scope.references[0]).to.be.equal(scope.variables[0].references[0]);

scope = scopeManager.scopes[1];
expect(scope.type).to.be.equal("class");
expect(scope.block.type).to.be.equal("ClassExpression");
expect(scope.isStrict).to.be.true;
expect(scope.variables).to.have.length(1);
expect(scope.variables[0].name).to.be.equal("A"); // variable `A` defined by `class A`
expect(scope.variables[0].references).to.have.length(1); // reference `A` in `extends A`
expect(scope.references).to.have.length(1);
expect(scope.references[0].resolved).to.be.equal(scope.variables[0]);
expect(scope.references[0]).to.be.equal(scope.variables[0].references[0]);
});

it("class heritage may refer class name in class expressions #2", () => {
const ast = espree(`
let foo;
(class C extends (foo = C, class {}) {});
`);

const scopeManager = analyze(ast, { ecmaVersion: 6 });

expect(scopeManager.scopes).to.have.length(3);

let scope = scopeManager.scopes[0];

expect(scope.type).to.be.equal("global");
expect(scope.block.type).to.be.equal("Program");
expect(scope.isStrict).to.be.false;
expect(scope.variables).to.have.length(1);
expect(scope.variables[0].name).to.be.equal("foo");
expect(scope.references).to.have.length(0);

scope = scopeManager.scopes[1];
expect(scope.type).to.be.equal("class");
expect(scope.block.type).to.be.equal("ClassExpression");
expect(scope.isStrict).to.be.true;
expect(scope.variables).to.have.length(1);
expect(scope.variables[0].name).to.be.equal("C");
expect(scope.variables[0].references).to.have.length(1);
expect(scope.references).to.have.length(2);
expect(scope.references[0].identifier.name).to.be.equal("foo");
expect(scope.references[1].identifier.name).to.be.equal("C");

// `C` in `foo = C` is a reference to variable `C` defined by `class C`
expect(scope.references[1].resolved).to.be.equal(scope.variables[0]);
expect(scope.references[1]).to.be.equal(scope.variables[0].references[0]);

scope = scopeManager.scopes[2];
expect(scope.type).to.be.equal("class");
expect(scope.block.type).to.be.equal("ClassExpression");
expect(scope.isStrict).to.be.true;
expect(scope.variables).to.have.length(0);
expect(scope.references).to.have.length(0);
});

it("class heritage may refer class name in class declarations", () => {
const ast = espree(`
let foo;
class C extends (foo = C, class {}) {}
new C();
`);

const scopeManager = analyze(ast, { ecmaVersion: 6 });

expect(scopeManager.scopes).to.have.length(3);

let scope = scopeManager.scopes[0];

expect(scope.type).to.be.equal("global");
expect(scope.block.type).to.be.equal("Program");
expect(scope.isStrict).to.be.false;
expect(scope.variables).to.have.length(2);
expect(scope.variables[0].name).to.be.equal("foo");
expect(scope.variables[0].references).to.have.length(1);
expect(scope.variables[1].name).to.be.equal("C");
expect(scope.variables[1].references).to.have.length(1);
expect(scope.references).to.have.length(1);
expect(scope.references[0].identifier.name).to.be.equal("C"); // `C` in `new C()`
expect(scope.references[0].resolved).to.be.equal(scope.variables[1]);
expect(scope.references[0]).to.be.equal(scope.variables[1].references[0]);

scope = scopeManager.scopes[1];
expect(scope.type).to.be.equal("class");
expect(scope.block.type).to.be.equal("ClassDeclaration");
expect(scope.isStrict).to.be.true;
expect(scope.variables).to.have.length(1);
expect(scope.variables[0].name).to.be.equal("C");
expect(scope.variables[0].references).to.have.length(1);
expect(scope.references).to.have.length(2);
expect(scope.references[0].identifier.name).to.be.equal("foo");
expect(scope.references[1].identifier.name).to.be.equal("C"); // `C` in `foo = C`

/*
* `class C` creates two variables `C`: one in the scope where the class
* is declared, another in the class scope. References inside the class
* should be to the variable in the class scope.
*/
expect(scope.references[1].resolved).to.be.equal(scope.variables[0]);
expect(scope.references[1]).to.be.equal(scope.variables[0].references[0]);

scope = scopeManager.scopes[2];
expect(scope.type).to.be.equal("class");
expect(scope.block.type).to.be.equal("ClassExpression");
expect(scope.isStrict).to.be.true;
expect(scope.variables).to.have.length(0);
expect(scope.references).to.have.length(0);
});

it("inner scopes in the class heritage of a class expression are nested in the class scope", () => {
const ast = espree(`
(class extends function () {} {})
`);

const scopeManager = analyze(ast, { ecmaVersion: 6 });

expect(scopeManager.scopes).to.have.length(3);

let scope = scopeManager.scopes[0];

expect(scope.type).to.be.equal("global");
expect(scope.block.type).to.be.equal("Program");
expect(scope.isStrict).to.be.false;

scope = scopeManager.scopes[1];
expect(scope.type).to.be.equal("class");
expect(scope.block.type).to.be.equal("ClassExpression");
expect(scope.isStrict).to.be.true;

scope = scopeManager.scopes[2];
expect(scope.type).to.be.equal("function");
expect(scope.block.type).to.be.equal("FunctionExpression");
expect(scope.isStrict).to.be.true;
expect(scope.upper).to.be.equal(scopeManager.scopes[1]);
expect(scopeManager.scopes[1].childScopes).to.have.length(1);
expect(scopeManager.scopes[1].childScopes[0]).to.be.equal(scope);
});

it("inner scopes in the class heritage of a class declaration are nested in the class scope", () => {
const ast = espree(`
class C extends function () {} {}
`);

const scopeManager = analyze(ast, { ecmaVersion: 6 });

expect(scopeManager.scopes).to.have.length(3);

let scope = scopeManager.scopes[0];

expect(scope.type).to.be.equal("global");
expect(scope.block.type).to.be.equal("Program");
expect(scope.isStrict).to.be.false;

scope = scopeManager.scopes[1];
expect(scope.type).to.be.equal("class");
expect(scope.block.type).to.be.equal("ClassDeclaration");
expect(scope.isStrict).to.be.true;

scope = scopeManager.scopes[2];
expect(scope.type).to.be.equal("function");
expect(scope.block.type).to.be.equal("FunctionExpression");
expect(scope.isStrict).to.be.true;
expect(scope.upper).to.be.equal(scopeManager.scopes[1]);
expect(scopeManager.scopes[1].childScopes).to.have.length(1);
expect(scopeManager.scopes[1].childScopes[0]).to.be.equal(scope);
});

it("regression #49", () => {
const ast = espree(`
class Shoe {
Expand Down
6 changes: 3 additions & 3 deletions tests/es6-super.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ describe("ES6 super", () => {
expect(scope.type).to.be.equal("global");
expect(scope.variables).to.have.length(1);
expect(scope.variables[0].name).to.be.equal("Foo");
expect(scope.references).to.have.length(1);
expect(scope.references[0].identifier.name).to.be.equal("Bar");
expect(scope.references).to.have.length(0);

scope = scopeManager.scopes[1];
expect(scope.type).to.be.equal("class");
expect(scope.variables).to.have.length(1);
expect(scope.variables[0].name).to.be.equal("Foo");
expect(scope.references).to.have.length(0);
expect(scope.references).to.have.length(1);
expect(scope.references[0].identifier.name).to.be.equal("Bar");

scope = scopeManager.scopes[2];
expect(scope.type).to.be.equal("function");
Expand Down

0 comments on commit 42ef7a9

Please sign in to comment.