Skip to content
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

feat: update no-use-before-define for class static blocks #15312

Merged
merged 1 commit into from Nov 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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