Skip to content

Commit

Permalink
feat: update no-use-before-define for class static blocks
Browse files Browse the repository at this point in the history
Updates logic related to class definition evaluation with class static blocks.

Refs #15016
  • Loading branch information
mdjermanovic committed Nov 14, 2021
1 parent a2e6328 commit bb6e846
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 10 deletions.
34 changes: 34 additions & 0 deletions docs/rules/no-use-before-define.md
Expand Up @@ -45,6 +45,14 @@ var b = 1;
static x = C;
}
}

{
const C = class {
static {
C.x = "foo";
}
}
}
```

Examples of **correct** code for this rule:
Expand Down Expand Up @@ -86,6 +94,14 @@ function g() {
x = C;
}
}

{
const C = class C {
static {
C.x = "foo";
}
}
}
```

## Options
Expand Down Expand Up @@ -156,6 +172,15 @@ class A {
[C.x]() {}
}
}

{
class C {
static {
new D();
}
}
class D {}
}
```

Examples of **correct** code for the `{ "classes": false }` option:
Expand Down Expand Up @@ -199,6 +224,15 @@ const g = function() {};
}
const foo = 1;
}

{
class C {
static {
this.x = foo;
}
}
const foo = 1;
}
```

Examples of **correct** code for the `{ "variables": false }` option:
Expand Down
41 changes: 32 additions & 9 deletions lib/rules/no-use-before-define.js
Expand Up @@ -45,25 +45,37 @@ function isInRange(node, location) {

/**
* Checks whether or not a given location is inside of the range of a class static initializer.
* Static initializers are static blocks and initializers of static fields.
* @param {ASTNode} node `ClassBody` node to check static initializers.
* @param {number} location A location to check.
* @returns {boolean} `true` if the location is inside of a class static initializer.
*/
function isInClassStaticInitializerRange(node, location) {
return node.body.some(classMember => (
classMember.type === "PropertyDefinition" &&
classMember.static &&
classMember.value &&
isInRange(classMember.value, location)
(
classMember.type === "StaticBlock" &&
isInRange(classMember, location)
) ||
(
classMember.type === "PropertyDefinition" &&
classMember.static &&
classMember.value &&
isInRange(classMember.value, location)
)
));
}

/**
* Checks whether a given scope is the scope of a static class field initializer.
* Checks whether a given scope is the scope of a a class static initializer.
* Static initializers are static blocks and initializers of static fields.
* @param {eslint-scope.Scope} scope A scope to check.
* @returns {boolean} `true` if the scope is a class static initializer scope.
*/
function isClassStaticInitializerScope(scope) {
if (scope.type === "class-static-block") {
return true;
}

if (scope.type === "class-field-initializer") {

// `scope.block` is PropertyDefinition#value node
Expand All @@ -82,21 +94,31 @@ function isClassStaticInitializerScope(scope) {
* - top-level
* - functions
* - class field initializers (implicit functions)
* Static class field initializers are automatically run during the class definition evaluation,
* - class static blocks (implicit functions)
* Static class field initializers and class static blocks are automatically run during the class definition evaluation,
* and therefore we'll consider them as a part of the parent execution context.
* Example:
*
* const x = 1;
*
* x; // returns `false`
* () => x; // returns `true`
*
* class C {
* field = x; // returns `true`
* static field = x; // returns `false`
*
* method() {
* x; // returns `true`
* }
*
* static method() {
* x; // returns `true`
* }
*
* static {
* x; // returns `false`
* }
* }
* @param {eslint-scope.Reference} reference A reference to check.
* @returns {boolean} `true` if the reference is from a separate execution context.
Expand Down Expand Up @@ -127,8 +149,9 @@ function isFromSeparateExecutionContext(reference) {
* var {a = a} = obj
* for (var a in a) {}
* for (var a of a) {}
* var C = class { [C]; }
* var C = class { static foo = C; }
* var C = class { [C]; };
* var C = class { static foo = C; };
* var C = class { static { foo = C; } };
* class C extends C {}
* class C extends (class { static foo = C; }) {}
* class C { [C]; }
Expand Down Expand Up @@ -158,7 +181,7 @@ function isEvaluatedDuringInitialization(reference) {

/*
* Class binding is initialized before running static initializers.
* For example, `class C { static foo = C; }` is valid.
* For example, `class C { static foo = C; static { bar = C; } }` is valid.
*/
!isInClassStaticInitializerRange(classDefinition.body, location)
);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -56,7 +56,7 @@
"doctrine": "^3.0.0",
"enquirer": "^2.3.5",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^6.0.0",
"eslint-scope": "github:eslint/eslint-scope#prepare-v7",
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.1.0",
"espree": "^9.0.0",
Expand Down
116 changes: 116 additions & 0 deletions tests/lib/rules/no-use-before-define.js
Expand Up @@ -38,6 +38,8 @@ ruleTester.run("no-use-before-define", rule, {
"var foo = function() { foo(); };",
"var a; for (a in a) {}",
{ code: "var a; for (a of a) {}", parserOptions: { ecmaVersion: 6 } },
{ code: "let a; class C { static { a; } }", parserOptions: { ecmaVersion: 2022 } },
{ code: "class C { static { let a; a; } }", parserOptions: { ecmaVersion: 2022 } },

// Block-level bindings
{ code: "\"use strict\"; a(); { function a() {} }", parserOptions: { ecmaVersion: 6 } },
Expand All @@ -60,6 +62,11 @@ ruleTester.run("no-use-before-define", rule, {
options: [{ variables: false }],
parserOptions: { ecmaVersion: 6 }
},
{
code: "class C { static { () => foo; let foo; } }",
options: [{ variables: false }],
parserOptions: { ecmaVersion: 2022 }
},

// Tests related to class definition evaluation. These are not TDZ errors.
{ code: "class C extends (class { method() { C; } }) {}", parserOptions: { ecmaVersion: 6 } },
Expand Down Expand Up @@ -156,6 +163,52 @@ ruleTester.run("no-use-before-define", rule, {
code: "class C { static field = class { field = a; }; } let a;",
options: [{ variables: false }],
parserOptions: { ecmaVersion: 2022 }
},
{
code: "class C { static { C; } }", // `const C = class { static { C; } }` is TDZ error
parserOptions: { ecmaVersion: 2022 }
},
{
code: "class C { static { C; } static {} static { C; } }",
parserOptions: { ecmaVersion: 2022 }
},
{
code: "(class C { static { C; } })",
parserOptions: { ecmaVersion: 2022 }
},
{
code: "class C { static { class D extends C {} } }",
parserOptions: { ecmaVersion: 2022 }
},
{
code: "class C { static { (class { static { C } }) } }",
parserOptions: { ecmaVersion: 2022 }
},
{
code: "class C { static { () => C; } }",
parserOptions: { ecmaVersion: 2022 }
},
{
code: "(class C { static { () => C; } })",
parserOptions: { ecmaVersion: 2022 }
},
{
code: "const C = class { static { () => C; } }",
parserOptions: { ecmaVersion: 2022 }
},
{
code: "class C { static { () => D; } } class D {}",
options: [{ classes: false }],
parserOptions: { ecmaVersion: 2022 }
},
{
code: "class C { static { () => a; } } let a;",
options: [{ variables: false }],
parserOptions: { ecmaVersion: 2022 }
},
{
code: "const C = class C { static { C.x; } }",
parserOptions: { ecmaVersion: 2022 }
}
],
invalid: [
Expand Down Expand Up @@ -975,6 +1028,69 @@ ruleTester.run("no-use-before-define", rule, {
messageId: "usedBeforeDefined",
data: { name: "a" }
}]
},
{
code: "const C = class { static { C; } };",
options: [{ variables: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{
messageId: "usedBeforeDefined",
data: { name: "C" }
}]
},
{
code: "const C = class { static { (class extends C {}); } };",
options: [{ variables: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{
messageId: "usedBeforeDefined",
data: { name: "C" }
}]
},
{
code: "class C { static { a; } } let a;",
options: [{ variables: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{
messageId: "usedBeforeDefined",
data: { name: "a" }
}]
},
{
code: "class C { static { D; } } class D {}",
options: [{ classes: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{
messageId: "usedBeforeDefined",
data: { name: "D" }
}]
},
{
code: "class C { static { (class extends D {}); } } class D {}",
options: [{ classes: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{
messageId: "usedBeforeDefined",
data: { name: "D" }
}]
},
{
code: "class C { static { (class { [a](){} }); } } let a;",
options: [{ variables: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{
messageId: "usedBeforeDefined",
data: { name: "a" }
}]
},
{
code: "class C { static { (class { static field = a; }); } } let a;",
options: [{ variables: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{
messageId: "usedBeforeDefined",
data: { name: "a" }
}]
}

/*
Expand Down

0 comments on commit bb6e846

Please sign in to comment.