Repository: eslint/eslint
Pull request #15481 (2022-01-03 21:22:40)

feat: Support arbitrary module namespace names in keyword-spacing
<!--
    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 `keyword-spacing`.

<!--
    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)

Updated `keyword-spacing` rule to check spacing around `as` in import/export specifiers.

For example, after this change each of these `as` will be reported:

```js
/* eslint keyword-spacing: "error" */

import { "a"as b } from "mod"; // Expected space(s) before "as"

export { b as"c" }; // Expected space(s) after "as"

export { "d"as e } from "mod"; // Expected space(s) before "as"

export * as"f" from "mod"; // Expected space(s) after "as"

```

#### 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/keyword-spacing.md b/docs/rules/keyword-spacing.md
index 8ca673937..031873b5e 100644
--- a/docs/rules/keyword-spacing.md
+++ b/docs/rules/keyword-spacing.md
@@ -243,14 +243,15 @@ if(foo) {
 
 ### overrides
 
-Examples of **correct** code for this rule with the `{ "overrides": { "if": { "after": false }, "for": { "after": false }, "while": { "after": false }, "static": { "after": false } } }` option:
+Examples of **correct** code for this rule with the `{ "overrides": { "if": { "after": false }, "for": { "after": false }, "while": { "after": false }, "static": { "after": false }, "as": { "after": false } } }` option:
 
 ```js
 /*eslint keyword-spacing: ["error", { "overrides": {
   "if": { "after": false },
   "for": { "after": false },
   "while": { "after": false },
-  "static": { "after": false }
+  "static": { "after": false },
+  "as": { "after": false }
 } }]*/
 
 if(foo) {
@@ -272,6 +273,8 @@ class C {
         //...
     }
 }
+
+export { C as"my class" };
 ```
 
 ## When Not To Use It
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/keyword-spacing.js b/lib/rules/keyword-spacing.js
index 956fc4265..16a65d786 100644
--- a/lib/rules/keyword-spacing.js
+++ b/lib/rules/keyword-spacing.js
@@ -469,6 +469,7 @@ module.exports = {
                 const asToken = sourceCode.getTokenBefore(node.exported);
 
                 checkSpacingBefore(asToken, PREV_TOKEN_M);
+                checkSpacingAfter(asToken, NEXT_TOKEN_M);
             }
 
             if (node.source) {
@@ -479,6 +480,35 @@ module.exports = {
             }
         }
 
+        /**
+         * Reports `as` keyword of a given node if usage of spacing around this
+         * keyword is invalid.
+         * @param {ASTNode} node An `ImportSpecifier` node to check.
+         * @returns {void}
+         */
+        function checkSpacingForImportSpecifier(node) {
+            if (node.imported.range[0] !== node.local.range[0]) {
+                const asToken = sourceCode.getTokenBefore(node.local);
+
+                checkSpacingBefore(asToken, PREV_TOKEN_M);
+            }
+        }
+
+        /**
+         * Reports `as` keyword of a given node if usage of spacing around this
+         * keyword is invalid.
+         * @param {ASTNode} node An `ExportSpecifier` node to check.
+         * @returns {void}
+         */
+        function checkSpacingForExportSpecifier(node) {
+            if (node.local.range[0] !== node.exported.range[0]) {
+                const asToken = sourceCode.getTokenBefore(node.exported);
+
+                checkSpacingBefore(asToken, PREV_TOKEN_M);
+                checkSpacingAfter(asToken, NEXT_TOKEN_M);
+            }
+        }
+
         /**
          * Reports `as` keyword of a given node if usage of spacing around this
          * keyword is invalid.
@@ -588,6 +618,8 @@ module.exports = {
             YieldExpression: checkSpacingBeforeFirstToken,
 
             // Others
+            ImportSpecifier: checkSpacingForImportSpecifier,
+            ExportSpecifier: checkSpacingForExportSpecifier,
             ImportNamespaceSpecifier: checkSpacingForImportNamespaceSpecifier,
             MethodDefinition: checkSpacingForProperty,
             PropertyDefinition: checkSpacingForProperty,
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/keyword-spacing.js b/tests/lib/rules/keyword-spacing.js
index 35c8a06e4..97d198f56 100644
--- a/tests/lib/rules/keyword-spacing.js
+++ b/tests/lib/rules/keyword-spacing.js
@@ -126,14 +126,53 @@ ruleTester.run("keyword-spacing", rule, {
         // as
         //----------------------------------------------------------------------
 
+        // import { a as b }
+        { code: "import { a } from \"foo\"", parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "import { a as b } from \"foo\"", parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "import { \"a\" as b } from \"foo\"", parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "import{ a }from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "import{ a as b }from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "import{ \"a\"as b }from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "import{ \"a\" as b }from\"foo\"", options: [override("as", BOTH)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "import { a as b } from \"foo\"", options: [override("as", NEITHER)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "import { \"a\"as b } from \"foo\"", options: [override("as", NEITHER)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+
+        // export { a as b }
+        { code: "let a; export { a };", parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "export { \"a\" } from \"foo\";", parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "let a; export { a as b };", parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "let a; export { a as \"b\" };", parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "export { \"a\" as b } from \"foo\";", parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "export { \"a\" as \"b\" } from \"foo\";", parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "let a; export{ a };", options: [NEITHER], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "export{ \"a\" }from\"foo\";", options: [NEITHER], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "let a; export{ a as b };", options: [NEITHER], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "let a; export{ a as\"b\" };", options: [NEITHER], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "export{ \"a\"as b }from\"foo\";", options: [NEITHER], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "export{ \"a\"as\"b\" }from\"foo\";", options: [NEITHER], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "let a; export{ a as \"b\" };", options: [override("as", BOTH)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "export{ \"a\" as b }from\"foo\";", options: [override("as", BOTH)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "export{ \"a\" as \"b\" }from\"foo\";", options: [override("as", BOTH)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "let a; export { a as b };", options: [override("as", NEITHER)], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "let a; export { a as\"b\" };", options: [override("as", NEITHER)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "export { \"a\"as b } from \"foo\";", options: [override("as", NEITHER)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+        { code: "export { \"a\"as\"b\" } from \"foo\";", options: [override("as", NEITHER)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
+
+        // import * as a
         { code: "import * as a from \"foo\"", parserOptions: { ecmaVersion: 6, sourceType: "module" } },
         { code: "import*as a from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
         { code: "import* as a from\"foo\"", options: [override("as", BOTH)], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
         { code: "import *as a from \"foo\"", options: [override("as", NEITHER)], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+
+        // export * as a
         { code: "export * as a from \"foo\"", parserOptions: { ecmaVersion: 2020, sourceType: "module" } },
+        { code: "export * as \"a\" from \"foo\"", parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
         { code: "export*as a from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 2020, sourceType: "module" } },
+        { code: "export*as\"a\"from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
         { code: "export* as a from\"foo\"", options: [override("as", BOTH)], parserOptions: { ecmaVersion: 2020, sourceType: "module" } },
+        { code: "export* as \"a\"from\"foo\"", options: [override("as", BOTH)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
         { code: "export *as a from \"foo\"", options: [override("as", NEITHER)], parserOptions: { ecmaVersion: 2020, sourceType: "module" } },
+        { code: "export *as\"a\" from \"foo\"", options: [override("as", NEITHER)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
 
         //----------------------------------------------------------------------
         // async
@@ -654,15 +693,21 @@ ruleTester.run("keyword-spacing", rule, {
         { code: "import {foo} from \"foo\"", parserOptions: { ecmaVersion: 6, sourceType: "module" } },
         { code: "export {foo} from \"foo\"", parserOptions: { ecmaVersion: 6, sourceType: "module" } },
         { code: "export * from \"foo\"", parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "export * as \"x\" from \"foo\"", parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
         { code: "import{foo}from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
         { code: "export{foo}from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
         { code: "export*from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "export*as x from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 2020, sourceType: "module" } },
+        { code: "export*as\"x\"from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
         { code: "import{foo} from \"foo\"", options: [override("from", BOTH)], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
         { code: "export{foo} from \"foo\"", options: [override("from", BOTH)], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
         { code: "export* from \"foo\"", options: [override("from", BOTH)], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "export*as\"x\" from \"foo\"", options: [override("from", BOTH)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
         { code: "import {foo}from\"foo\"", options: [override("from", NEITHER)], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
         { code: "export {foo}from\"foo\"", options: [override("from", NEITHER)], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
         { code: "export *from\"foo\"", options: [override("from", NEITHER)], parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "export * as x from\"foo\"", options: [override("from", NEITHER)], parserOptions: { ecmaVersion: 2020, sourceType: "module" } },
+        { code: "export * as \"x\"from\"foo\"", options: [override("from", NEITHER)], parserOptions: { ecmaVersion: 2022, sourceType: "module" } },
 
         //----------------------------------------------------------------------
         // function
@@ -1469,6 +1514,119 @@ ruleTester.run("keyword-spacing", rule, {
         // as
         //----------------------------------------------------------------------
 
+        // import { a as b }
+        {
+            code: "import { \"a\"as b } from \"foo\"",
+            output: "import { \"a\" as b } from \"foo\"",
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: expectedBefore("as")
+        },
+        {
+            code: "import{ \"a\" as b }from\"foo\"",
+            output: "import{ \"a\"as b }from\"foo\"",
+            options: [NEITHER],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: unexpectedBefore("as")
+        },
+        {
+            code: "import{ \"a\"as b }from\"foo\"",
+            output: "import{ \"a\" as b }from\"foo\"",
+            options: [override("as", BOTH)],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: expectedBefore("as")
+        },
+        {
+            code: "import { \"a\" as b } from \"foo\"",
+            output: "import { \"a\"as b } from \"foo\"",
+            options: [override("as", NEITHER)],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: unexpectedBefore("as")
+        },
+
+        // export { a as b }
+        {
+            code: "let a; export { a as\"b\" };",
+            output: "let a; export { a as \"b\" };",
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: expectedAfter("as")
+        },
+        {
+            code: "export { \"a\"as b } from \"foo\";",
+            output: "export { \"a\" as b } from \"foo\";",
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: expectedBefore("as")
+        },
+        {
+            code: "export { \"a\"as\"b\" } from \"foo\";",
+            output: "export { \"a\" as \"b\" } from \"foo\";",
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: expectedBeforeAndAfter("as")
+        },
+        {
+            code: "let a; export{ a as \"b\" };",
+            output: "let a; export{ a as\"b\" };",
+            options: [NEITHER],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: unexpectedAfter("as")
+        },
+        {
+            code: "export{ \"a\" as b }from\"foo\";",
+            output: "export{ \"a\"as b }from\"foo\";",
+            options: [NEITHER],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: unexpectedBefore("as")
+        },
+        {
+            code: "export{ \"a\" as \"b\" }from\"foo\";",
+            output: "export{ \"a\"as\"b\" }from\"foo\";",
+            options: [NEITHER],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: unexpectedBeforeAndAfter("as")
+        },
+        {
+            code: "let a; export{ a as\"b\" };",
+            output: "let a; export{ a as \"b\" };",
+            options: [override("as", BOTH)],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: expectedAfter("as")
+        },
+        {
+            code: "export{ \"a\"as b }from\"foo\";",
+            output: "export{ \"a\" as b }from\"foo\";",
+            options: [override("as", BOTH)],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: expectedBefore("as")
+        },
+        {
+            code: "export{ \"a\"as\"b\" }from\"foo\";",
+            output: "export{ \"a\" as \"b\" }from\"foo\";",
+            options: [override("as", BOTH)],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: expectedBeforeAndAfter("as")
+        },
+        {
+            code: "let a; export { a as \"b\" };",
+            output: "let a; export { a as\"b\" };",
+            options: [override("as", NEITHER)],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: unexpectedAfter("as")
+        },
+        {
+            code: "export { \"a\" as b } from \"foo\";",
+            output: "export { \"a\"as b } from \"foo\";",
+            options: [override("as", NEITHER)],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: unexpectedBefore("as")
+        },
+        {
+            code: "export { \"a\" as \"b\" } from \"foo\";",
+            output: "export { \"a\"as\"b\" } from \"foo\";",
+            options: [override("as", NEITHER)],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: unexpectedBeforeAndAfter("as")
+        },
+
+        // import * as a
         {
             code: "import *as a from \"foo\"",
             output: "import * as a from \"foo\"",
@@ -1524,12 +1682,20 @@ ruleTester.run("keyword-spacing", rule, {
             parserOptions: { ecmaVersion: 6, sourceType: "module" },
             errors: unexpectedBefore("as")
         },
+
+        // export * as a
         {
             code: "export *as a from \"foo\"",
             output: "export * as a from \"foo\"",
             parserOptions: { ecmaVersion: 2020, sourceType: "module" },
             errors: expectedBefore("as")
         },
+        {
+            code: "export *as\"a\" from \"foo\"",
+            output: "export * as \"a\" from \"foo\"",
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: expectedBeforeAndAfter("as")
+        },
         {
             code: "export* as a from\"foo\"",
             output: "export*as a from\"foo\"",
@@ -1537,6 +1703,13 @@ ruleTester.run("keyword-spacing", rule, {
             parserOptions: { ecmaVersion: 2020, sourceType: "module" },
             errors: unexpectedBefore("as")
         },
+        {
+            code: "export* as \"a\"from\"foo\"",
+            output: "export*as\"a\"from\"foo\"",
+            options: [NEITHER],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: unexpectedBeforeAndAfter("as")
+        },
         {
             code: "export*as a from\"foo\"",
             output: "export* as a from\"foo\"",
@@ -1544,6 +1717,13 @@ ruleTester.run("keyword-spacing", rule, {
             parserOptions: { ecmaVersion: 2020, sourceType: "module" },
             errors: expectedBefore("as")
         },
+        {
+            code: "export*as\"a\"from\"foo\"",
+            output: "export* as \"a\"from\"foo\"",
+            options: [override("as", BOTH)],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: expectedBeforeAndAfter("as")
+        },
         {
             code: "export * as a from \"foo\"",
             output: "export *as a from \"foo\"",
@@ -1551,6 +1731,13 @@ ruleTester.run("keyword-spacing", rule, {
             parserOptions: { ecmaVersion: 2020, sourceType: "module" },
             errors: unexpectedBefore("as")
         },
+        {
+            code: "export * as \"a\" from \"foo\"",
+            output: "export *as\"a\" from \"foo\"",
+            options: [override("as", NEITHER)],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: unexpectedBeforeAndAfter("as")
+        },
 
         //----------------------------------------------------------------------
         // async
@@ -2399,6 +2586,12 @@ ruleTester.run("keyword-spacing", rule, {
             parserOptions: { ecmaVersion: 6, sourceType: "module" },
             errors: expectedBeforeAndAfter("from")
         },
+        {
+            code: "export * as \"a\"from\"foo\"",
+            output: "export * as \"a\" from \"foo\"",
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: expectedBeforeAndAfter("from")
+        },
         {
             code: "import{foo} from \"foo\"",
             output: "import{foo}from\"foo\"",
@@ -2420,6 +2613,20 @@ ruleTester.run("keyword-spacing", rule, {
             parserOptions: { ecmaVersion: 6, sourceType: "module" },
             errors: unexpectedBeforeAndAfter("from")
         },
+        {
+            code: "export*as x from \"foo\"",
+            output: "export*as x from\"foo\"",
+            options: [NEITHER],
+            parserOptions: { ecmaVersion: 2020, sourceType: "module" },
+            errors: unexpectedAfter("from")
+        },
+        {
+            code: "export*as\"x\" from \"foo\"",
+            output: "export*as\"x\"from\"foo\"",
+            options: [NEITHER],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: unexpectedBeforeAndAfter("from")
+        },
         {
             code: "import{foo}from\"foo\"",
             output: "import{foo} from \"foo\"",
@@ -2441,6 +2648,13 @@ ruleTester.run("keyword-spacing", rule, {
             parserOptions: { ecmaVersion: 6, sourceType: "module" },
             errors: expectedBeforeAndAfter("from")
         },
+        {
+            code: "export*as\"x\"from\"foo\"",
+            output: "export*as\"x\" from \"foo\"",
+            options: [override("from", BOTH)],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: expectedBeforeAndAfter("from")
+        },
         {
             code: "import {foo} from \"foo\"",
             output: "import {foo}from\"foo\"",
@@ -2462,6 +2676,20 @@ ruleTester.run("keyword-spacing", rule, {
             parserOptions: { ecmaVersion: 6, sourceType: "module" },
             errors: unexpectedBeforeAndAfter("from")
         },
+        {
+            code: "export * as x from \"foo\"",
+            output: "export * as x from\"foo\"",
+            options: [override("from", NEITHER)],
+            parserOptions: { ecmaVersion: 2020, sourceType: "module" },
+            errors: unexpectedAfter("from")
+        },
+        {
+            code: "export * as \"x\" from \"foo\"",
+            output: "export * as \"x\"from\"foo\"",
+            options: [override("from", NEITHER)],
+            parserOptions: { ecmaVersion: 2022, sourceType: "module" },
+            errors: unexpectedBeforeAndAfter("from")
+        },
 
         //----------------------------------------------------------------------
         // function
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] = [];",