From f6d79209821241c8e03c183b5844a024da0efe8a Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 3 Jun 2022 22:28:07 +0200 Subject: [PATCH] feat: add `allowNamedExports` option to no-use-before-define (#15953) Fixes #15710 --- docs/src/_data/further_reading_links.json | 2 +- docs/src/rules/no-use-before-define.md | 54 +++++++- lib/rules/no-use-before-define.js | 17 ++- tests/lib/rules/no-use-before-define.js | 153 +++++++++++++++++++++- 4 files changed, 220 insertions(+), 6 deletions(-) diff --git a/docs/src/_data/further_reading_links.json b/docs/src/_data/further_reading_links.json index 1ace5fff6e2..a57f9ff81bb 100644 --- a/docs/src/_data/further_reading_links.json +++ b/docs/src/_data/further_reading_links.json @@ -692,4 +692,4 @@ "title": "Exponentiation (**) - JavaScript | MDN", "description": "The exponentiation operator (**) returns the result of raising the first operand to the power of the second operand. It is equivalent to Math.pow, except it also accepts BigInts as operands." } -} +} \ No newline at end of file diff --git a/docs/src/rules/no-use-before-define.md b/docs/src/rules/no-use-before-define.md index f388a469540..563ee651c2a 100644 --- a/docs/src/rules/no-use-before-define.md +++ b/docs/src/rules/no-use-before-define.md @@ -60,6 +60,9 @@ var b = 1; } } } + +export { foo }; +const foo = 1; ``` Examples of **correct** code for this rule: @@ -109,13 +112,21 @@ function g() { } } } + +const foo = 1; +export { foo }; ``` ## Options ```json { - "no-use-before-define": ["error", { "functions": true, "classes": true, "variables": true }] + "no-use-before-define": ["error", { + "functions": true, + "classes": true, + "variables": true, + "allowNamedExports": false + }] } ``` @@ -136,9 +147,13 @@ function g() { If this is `true`, the rule warns every reference to a variable before the variable declaration. Otherwise, the rule ignores a reference if the declaration is in an upper scope, while still reporting the reference if it's in the same scope as the declaration. Default is `true`. +* `allowNamedExports` (`boolean`) - + If this flag is set to `true`, the rule always allows references in `export {};` declarations. + These references are safe even if the variables are declared later in the code. + Default is `false`. This rule accepts `"nofunc"` string as an option. -`"nofunc"` is the same as `{ "functions": false, "classes": true, "variables": true }`. +`"nofunc"` is the same as `{ "functions": false, "classes": true, "variables": true, "allowNamedExports": false }`. ### functions @@ -267,3 +282,38 @@ const g = function() {} const foo = 1; } ``` + +### allowNamedExports + +Examples of **correct** code for the `{ "allowNamedExports": true }` option: + +```js +/*eslint no-use-before-define: ["error", { "allowNamedExports": true }]*/ + +export { a, b, f, C }; + +const a = 1; + +let b; + +function f () {} + +class C {} +``` + +Examples of **incorrect** code for the `{ "allowNamedExports": true }` option: + +```js +/*eslint no-use-before-define: ["error", { "allowNamedExports": true }]*/ + +export default a; +const a = 1; + +const b = c; +export const c = 1; + +export function foo() { + return d; +} +const d = 1; +``` diff --git a/lib/rules/no-use-before-define.js b/lib/rules/no-use-before-define.js index 07d035c431f..0dc4d76817b 100644 --- a/lib/rules/no-use-before-define.js +++ b/lib/rules/no-use-before-define.js @@ -21,6 +21,7 @@ function parseOptions(options) { let functions = true; let classes = true; let variables = true; + let allowNamedExports = false; if (typeof options === "string") { functions = (options !== "nofunc"); @@ -28,9 +29,10 @@ function parseOptions(options) { functions = options.functions !== false; classes = options.classes !== false; variables = options.variables !== false; + allowNamedExports = !!options.allowNamedExports; } - return { functions, classes, variables }; + return { functions, classes, variables, allowNamedExports }; } /** @@ -240,7 +242,8 @@ module.exports = { properties: { functions: { type: "boolean" }, classes: { type: "boolean" }, - variables: { type: "boolean" } + variables: { type: "boolean" }, + allowNamedExports: { type: "boolean" } }, additionalProperties: false } @@ -273,6 +276,16 @@ module.exports = { return false; } + const { identifier } = reference; + + if ( + options.allowNamedExports && + identifier.parent.type === "ExportSpecifier" && + identifier.parent.local === identifier + ) { + return false; + } + const variable = reference.resolved; if (!variable || variable.defs.length === 0) { diff --git a/tests/lib/rules/no-use-before-define.js b/tests/lib/rules/no-use-before-define.js index ba80e4075c2..dc858f75287 100644 --- a/tests/lib/rules/no-use-before-define.js +++ b/tests/lib/rules/no-use-before-define.js @@ -209,6 +209,38 @@ ruleTester.run("no-use-before-define", rule, { { code: "const C = class C { static { C.x; } }", parserOptions: { ecmaVersion: 2022 } + }, + + // "allowNamedExports" option + { + code: "export { a }; const a = 1;", + options: [{ allowNamedExports: true }], + parserOptions: { ecmaVersion: 2015, sourceType: "module" } + }, + { + code: "export { a as b }; const a = 1;", + options: [{ allowNamedExports: true }], + parserOptions: { ecmaVersion: 2015, sourceType: "module" } + }, + { + code: "export { a, b }; let a, b;", + options: [{ allowNamedExports: true }], + parserOptions: { ecmaVersion: 2015, sourceType: "module" } + }, + { + code: "export { a }; var a;", + options: [{ allowNamedExports: true }], + parserOptions: { ecmaVersion: 2015, sourceType: "module" } + }, + { + code: "export { f }; function f() {}", + options: [{ allowNamedExports: true }], + parserOptions: { ecmaVersion: 2015, sourceType: "module" } + }, + { + code: "export { C }; class C {}", + options: [{ allowNamedExports: true }], + parserOptions: { ecmaVersion: 2015, sourceType: "module" } } ], invalid: [ @@ -1091,7 +1123,7 @@ ruleTester.run("no-use-before-define", rule, { messageId: "usedBeforeDefined", data: { name: "a" } }] - } + }, /* * TODO(mdjermanovic): Add the following test cases once https://github.com/eslint/eslint-scope/issues/59 gets fixed: @@ -1123,5 +1155,124 @@ ruleTester.run("no-use-before-define", rule, { * }] * } */ + + // "allowNamedExports" option + { + code: "export { a }; const a = 1;", + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [{ + messageId: "usedBeforeDefined", + data: { name: "a" } + }] + }, + { + code: "export { a }; const a = 1;", + options: [{}], + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [{ + messageId: "usedBeforeDefined", + data: { name: "a" } + }] + }, + { + code: "export { a }; const a = 1;", + options: [{ allowNamedExports: false }], + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [{ + messageId: "usedBeforeDefined", + data: { name: "a" } + }] + }, + { + code: "export { a }; const a = 1;", + options: ["nofunc"], + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [{ + messageId: "usedBeforeDefined", + data: { name: "a" } + }] + }, + { + code: "export { a as b }; const a = 1;", + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [{ + messageId: "usedBeforeDefined", + data: { name: "a" } + }] + }, + { + code: "export { a, b }; let a, b;", + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [ + { + messageId: "usedBeforeDefined", + data: { name: "a" } + }, + { + messageId: "usedBeforeDefined", + data: { name: "b" } + } + ] + }, + { + code: "export { a }; var a;", + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [{ + messageId: "usedBeforeDefined", + data: { name: "a" } + }] + }, + { + code: "export { f }; function f() {}", + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [{ + messageId: "usedBeforeDefined", + data: { name: "f" } + }] + }, + { + code: "export { C }; class C {}", + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [{ + messageId: "usedBeforeDefined", + data: { name: "C" } + }] + }, + { + code: "export const foo = a; const a = 1;", + options: [{ allowNamedExports: true }], + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [{ + messageId: "usedBeforeDefined", + data: { name: "a" } + }] + }, + { + code: "export default a; const a = 1;", + options: [{ allowNamedExports: true }], + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [{ + messageId: "usedBeforeDefined", + data: { name: "a" } + }] + }, + { + code: "export function foo() { return a; }; const a = 1;", + options: [{ allowNamedExports: true }], + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [{ + messageId: "usedBeforeDefined", + data: { name: "a" } + }] + }, + { + code: "export class C { foo() { return a; } }; const a = 1;", + options: [{ allowNamedExports: true }], + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [{ + messageId: "usedBeforeDefined", + data: { name: "a" } + }] + } ] });