diff --git a/docs/rules/no-restricted-exports.md b/docs/rules/no-restricted-exports.md index 29938579d7e4..667a9c3fd171 100644 --- a/docs/rules/no-restricted-exports.md +++ b/docs/rules/no-restricted-exports.md @@ -18,7 +18,7 @@ Examples of **incorrect** code for this rule: ```js /*eslint no-restricted-exports: ["error", { - "restrictedNamedExports": ["foo", "bar", "Baz", "a", "b", "c", "d"] + "restrictedNamedExports": ["foo", "bar", "Baz", "a", "b", "c", "d", "e", "👍"] }]*/ export const foo = 1; @@ -33,16 +33,20 @@ export { a }; function someFunction() {} export { someFunction as b }; -export { c } from 'some_module'; +export { c } from "some_module"; -export { something as d } from 'some_module'; +export { "d" } from "some_module"; + +export { something as e } from "some_module"; + +export { "👍" } from "some_module"; ``` Examples of **correct** code for this rule: ```js /*eslint no-restricted-exports: ["error", { - "restrictedNamedExports": ["foo", "bar", "Baz", "a", "b", "c", "d"] + "restrictedNamedExports": ["foo", "bar", "Baz", "a", "b", "c", "d", "e", "👍"] }]*/ export const quux = 1; @@ -57,9 +61,13 @@ export { a as myObject }; function someFunction() {} export { someFunction }; -export { c as someName } from 'some_module'; +export { c as someName } from "some_module"; + +export { "d" as " d " } from "some_module"; + +export { something } from "some_module"; -export { something } from 'some_module'; +export { "👍" as thumbsUp } from "some_module"; ``` ### Default exports @@ -79,7 +87,7 @@ export { foo as default }; ```js /*eslint no-restricted-exports: ["error", { "restrictedNamedExports": ["default"] }]*/ -export { default } from 'some_module'; +export { default } from "some_module"; ``` Examples of additional **correct** code for this rule: @@ -102,5 +110,5 @@ export function foo() {} //----- my_module.js ----- /*eslint no-restricted-exports: ["error", { "restrictedNamedExports": ["foo"] }]*/ -export * from 'some_module'; // allowed, although this declaration exports "foo" from my_module +export * from "some_module"; // allowed, although this declaration exports "foo" from my_module ``` diff --git a/lib/rules/no-restricted-exports.js b/lib/rules/no-restricted-exports.js index 775e505d846c..5166cecaef2d 100644 --- a/lib/rules/no-restricted-exports.js +++ b/lib/rules/no-restricted-exports.js @@ -5,6 +5,12 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -44,12 +50,12 @@ module.exports = { const restrictedNames = new Set(context.options[0] && context.options[0].restrictedNamedExports); /** - * Checks and reports given exported identifier. - * @param {ASTNode} node exported `Identifier` node to check. + * Checks and reports given exported name. + * @param {ASTNode} node exported `Identifier` or string `Literal` node to check. * @returns {void} */ function checkExportedName(node) { - const name = node.name; + const name = astUtils.getModuleExportName(node); if (restrictedNames.has(name)) { context.report({ diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index 16d7b8115714..ecde099fa02d 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -769,6 +769,25 @@ function getSwitchCaseColonToken(node, sourceCode) { return sourceCode.getFirstToken(node, 1); } +/** + * Gets ESM module export name represented by the given node. + * @param {ASTNode} node `Identifier` or string `Literal` node in a position + * that represents a module export name: + * - `ImportSpecifier#imported` + * - `ExportSpecifier#local` (if it is a re-export from another module) + * - `ExportSpecifier#exported` + * - `ExportAllDeclaration#exported` + * @returns {string} The module export name. + */ +function getModuleExportName(node) { + if (node.type === "Identifier") { + return node.name; + } + + // string literal + return node.value; +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -1898,5 +1917,6 @@ module.exports = { equalLiteralValue, isSameReference, isLogicalAssignmentOperator, - getSwitchCaseColonToken + getSwitchCaseColonToken, + getModuleExportName }; diff --git a/tests/lib/rules/no-restricted-exports.js b/tests/lib/rules/no-restricted-exports.js index ac0602a25b29..6cde658107d6 100644 --- a/tests/lib/rules/no-restricted-exports.js +++ b/tests/lib/rules/no-restricted-exports.js @@ -16,7 +16,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020, sourceType: "module" } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2022, sourceType: "module" } }); ruleTester.run("no-restricted-exports", rule, { valid: [ @@ -57,8 +57,12 @@ ruleTester.run("no-restricted-exports", rule, { { code: "var b; export { b as a };", options: [{ restrictedNamedExports: ["x"] }] }, { code: "export { a } from 'foo';", options: [{ restrictedNamedExports: ["x"] }] }, { code: "export { b as a } from 'foo';", options: [{ restrictedNamedExports: ["x"] }] }, + { code: "export { '' } from 'foo';", options: [{ restrictedNamedExports: ["undefined"] }] }, + { code: "export { '' } from 'foo';", options: [{ restrictedNamedExports: [" "] }] }, + { code: "export { ' ' } from 'foo';", options: [{ restrictedNamedExports: [""] }] }, + { code: "export { ' a', 'a ' } from 'foo';", options: [{ restrictedNamedExports: ["a"] }] }, - // does not mistakenly disallow non-exported identifiers that appear in named export declarations + // does not mistakenly disallow non-exported names that appear in named export declarations { code: "export var b = a;", options: [{ restrictedNamedExports: ["a"] }] }, { code: "export let [b = a] = [];", options: [{ restrictedNamedExports: ["a"] }] }, { code: "export const [b] = [a];", options: [{ restrictedNamedExports: ["a"] }] }, @@ -69,7 +73,10 @@ ruleTester.run("no-restricted-exports", rule, { { code: "export class A { a(){} }", options: [{ restrictedNamedExports: ["a"] }] }, { code: "export class A extends B {}", options: [{ restrictedNamedExports: ["B"] }] }, { code: "var a; export { a as b };", options: [{ restrictedNamedExports: ["a"] }] }, + { code: "var a; export { a as 'a ' };", options: [{ restrictedNamedExports: ["a"] }] }, { code: "export { a as b } from 'foo';", options: [{ restrictedNamedExports: ["a"] }] }, + { code: "export { a as 'a ' } from 'foo';", options: [{ restrictedNamedExports: ["a"] }] }, + { code: "export { 'a' as 'a ' } from 'foo';", options: [{ restrictedNamedExports: ["a"] }] }, // does not check source in re-export declarations { code: "export { b } from 'a';", options: [{ restrictedNamedExports: ["a"] }] }, @@ -188,6 +195,59 @@ ruleTester.run("no-restricted-exports", rule, { errors: [{ messageId: "restrictedNamed", data: { name: "a" }, type: "Identifier" }] }, + // string literals + { + code: "let a; export { a as 'a' };", + options: [{ restrictedNamedExports: ["a"] }], + errors: [{ messageId: "restrictedNamed", data: { name: "a" }, type: "Literal", column: 22 }] + }, + { + code: "let a; export { a as 'b' };", + options: [{ restrictedNamedExports: ["b"] }], + errors: [{ messageId: "restrictedNamed", data: { name: "b" }, type: "Literal", column: 22 }] + }, + { + code: "let a; export { a as ' b ' };", + options: [{ restrictedNamedExports: [" b "] }], + errors: [{ messageId: "restrictedNamed", data: { name: " b " }, type: "Literal", column: 22 }] + }, + { + code: "let a; export { a as '👍' };", + options: [{ restrictedNamedExports: ["👍"] }], + errors: [{ messageId: "restrictedNamed", data: { name: "👍" }, type: "Literal", column: 22 }] + }, + { + code: "export { 'a' } from 'foo';", + options: [{ restrictedNamedExports: ["a"] }], + errors: [{ messageId: "restrictedNamed", data: { name: "a" }, type: "Literal" }] + }, + { + code: "export { '' } from 'foo';", + options: [{ restrictedNamedExports: [""] }], + errors: [{ messageId: "restrictedNamed", data: { name: "" }, type: "Literal" }] + }, + { + code: "export { ' ' } from 'foo';", + options: [{ restrictedNamedExports: [" "] }], + errors: [{ messageId: "restrictedNamed", data: { name: " " }, type: "Literal" }] + }, + { + code: "export { b as 'a' } from 'foo';", + options: [{ restrictedNamedExports: ["a"] }], + errors: [{ messageId: "restrictedNamed", data: { name: "a" }, type: "Literal" }] + }, + { + code: "export { b as '\\u0061' } from 'foo';", + options: [{ restrictedNamedExports: ["a"] }], + errors: [{ messageId: "restrictedNamed", data: { name: "a" }, type: "Literal" }] + }, + { + code: "export * as 'a' from 'foo';", + options: [{ restrictedNamedExports: ["a"] }], + errors: [{ messageId: "restrictedNamed", data: { name: "a" }, type: "Literal" }] + }, + + // destructuring { code: "export var [a] = [];",