Repository: eslint/eslint
Pull request #15478 (2022-01-02 13:21:12)

feat: Support arbitrary module namespace names in no-restricted-exports
<!--
    Thank you for contributing!

    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

#### Prerequisites checklist

- [x] I have read the [contributing guidelines](https://github.com/eslint/eslint/blob/HEAD/CONTRIBUTING.md).

#### What is the purpose of this pull request? (put an "X" next to an item)

<!--
    The following template is intentionally not a markdown checkbox list for the reasons
    explained in https://github.com/eslint/eslint/pull/12848#issuecomment-580302888
-->

[ ] Documentation update
[ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/HEAD/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/HEAD/templates/rule-proposal.md))
[x] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/HEAD/templates/rule-change-proposal.md))
[ ] Add autofix to a rule
[ ] Add a CLI option
[ ] Add something to the core
[ ] Other, please explain:

Refs #15465, fixes `no-restricted-exports`.

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (https://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

#### What changes did you make? (Give an overview)

Added a helper function `astUtils.getModuleExportName`, and updated `no-restricted-exports` rule to support tha new syntax.

#### Is there anything you'd like reviewers to focus on?

<!-- markdownlint-disable-file MD004 -->



Patch:
diff --git a/.github/ISSUE_TEMPLATE/NEW_SYNTAX.md b/.github/ISSUE_TEMPLATE/NEW_SYNTAX.md
deleted file mode 100644
index f3ac551f6..000000000
--- a/.github/ISSUE_TEMPLATE/NEW_SYNTAX.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-name: "\U0001F4DD Request new syntax support"
-about: Request new stage 4 syntax be supported.
-title: ''
-labels:
-    - core
-    - new syntax
-assignees: ''
-
----
-
-<!--
-    ESLint adheres to the Open JS Foundation Code of Conduct:
-    https://eslint.org/conduct
--->
-
-**What is the name of the syntax to implement?**
-
-<!-- for example, "class fields" -->
-
-**Please provide the TC39 URL for the syntax proposal:**
-
-<!-- for example, <https://github.com/tc39/proposal-top-level-await> -->
-
-**Please provide some example code for the new syntax:**
-
-```js
-// example code here
-```
-
-## Implementation Checklist
-
-Please check off all items that have already been completed. Be sure to paste the pull request URLs next to each item so we can verify the work as done.
-
-- [ ] Ecma262 update: <!-- paste PR URL for this syntax here -->
-- [ ] ESTree update: <!-- paste PR URL for this ESTree update here -->
-- [ ] Acorn update: <!-- paste PR URL for this syntax here -->
-- [ ] `eslint-visitor-keys` update: <!-- paste PR URL for this syntax here -->
-- [ ] `espree` update: <!-- paste PR URL for this syntax here -->
-- [ ] `eslint-scope` update: <!-- paste PR URL for this syntax here -->
-- [ ] `eslint` update: <!-- paste PR URL for this syntax here -->
-
-**Are you willing to submit a pull request to implement this syntax?**
-
-<!-- markdownlint-disable-file MD004 -->
diff --git a/.github/ISSUE_TEMPLATE/new-syntax.yml b/.github/ISSUE_TEMPLATE/new-syntax.yml
new file mode 100644
index 000000000..467ff0f2b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/new-syntax.yml
@@ -0,0 +1,60 @@
+name: "\U0001F4DD Request for new syntax support"
+description: "Request new stage 4 syntax be supported."
+labels:
+  - core
+  - new syntax
+body:
+- type: markdown
+  attributes:
+    value: By opening an issue, you agree to abide by the [Open JS Foundation Code of Conduct](https://eslint.org/conduct).
+- type: input
+  attributes:
+    label: Syntax name
+    description: What is the name of the syntax to implement?
+    placeholder: e.g. "class fields"
+  validations:
+    required: true
+- type: textarea
+  attributes:
+    label: Syntax proposal URL
+    description: Please provide the TC39 URL for the syntax proposal.
+    placeholder: e.g. https://github.com/tc39/proposal-top-level-await
+  validations:
+    required: true
+- type: textarea
+  attributes:
+    label: Example code
+    description: Please provide some example code for the new syntax.
+    render: js
+  validations:
+    required: true
+- type: checkboxes
+  attributes:
+    label: Implementation Checklist
+    description: |
+      Please check off all items that have already been completed. Be sure to paste the pull request URLs next to each item so we can verify the work as done.
+    options:
+      - label: "Ecma262 update: <!-- paste PR URL for this syntax here -->"
+        required: false
+      - label: "ESTree update: <!-- paste PR URL for this ESTree update here -->"
+        required: false
+      - label: "Acorn update: <!-- paste PR URL for this syntax here -->"
+        required: false
+      - label: "`eslint-visitor-keys` update: <!-- paste PR URL for this syntax here -->"
+        required: false
+      - label: "`espree` update: <!-- paste PR URL for this syntax here -->"
+        required: false
+      - label: "`eslint-scope` update: <!-- paste PR URL for this syntax here -->"
+        required: false
+      - label: "`eslint` update: <!-- paste PR URL for this syntax here -->"
+        required: false
+- type: checkboxes
+  attributes:
+    label: Participation
+    options:
+      - label: I am willing to submit a pull request for this change.
+        required: false
+- type: textarea
+  attributes:
+    label: Additional comments
+    description: Is there anything else that's important for the team to know?
diff --git a/docs/rules/no-restricted-exports.md b/docs/rules/no-restricted-exports.md
index 29938579d..667a9c3fd 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 775e505d8..5166cecae 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 16d7b8115..ecde099fa 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
 };


Test patch:
diff --git a/tests/lib/rules/no-restricted-exports.js b/tests/lib/rules/no-restricted-exports.js
index ac0602a25..6cde65810 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] = [];",