Repository: un-ts/eslint-plugin-import-x
Pull request #133 (2024-08-26 23:19:09)

fix(#123): make `no-named-as-default` ignores TS `namespace`

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 05f36a845dff8fee5ad169ebc6e53a89921f1a43

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/SukkaW/eslint-plugin-import-x/new/fix-123?filename=.changeset/long-crabs-scream.md&value=---%0A%22eslint-plugin-import-x%22%3A%20patch%0A---%0A%0Atest%3A%20add%20test%20cases%20for%20%23123%0A)


codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/un-ts/eslint-plugin-import-x/pr/133/builds/535497) or the icon next to each commit SHA.

Patch:
diff --git a/.changeset/hungry-paws-whisper.md b/.changeset/hungry-paws-whisper.md
new file mode 100644
index 00000000..dd31a673
--- /dev/null
+++ b/.changeset/hungry-paws-whisper.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+Fix #123 where the rule `no-named-as-default` will confuse TypeScript namespace exports with actual exports.
diff --git a/package.json b/package.json
index 5c0333bf..0ad9a047 100644
--- a/package.json
+++ b/package.json
@@ -89,6 +89,7 @@
     "@types/is-glob": "^4.0.4",
     "@types/jest": "^29.5.12",
     "@types/json-schema": "^7.0.15",
+    "@types/klaw-sync": "^6.0.5",
     "@types/node": "^20.11.30",
     "@typescript-eslint/eslint-plugin": "^8.1.0",
     "@typescript-eslint/parser": "^8.1.0",
@@ -114,6 +115,7 @@
     "eslint9": "npm:eslint@^9.8.0",
     "hermes-eslint": "^0.23.1",
     "jest": "^29.7.0",
+    "klaw-sync": "^6.0.0",
     "npm-run-all2": "^6.1.2",
     "prettier": "^3.2.5",
     "redux": "^5.0.1",
@@ -121,6 +123,7 @@
     "svelte": "^4.2.12",
     "ts-node": "^10.9.2",
     "type-fest": "^4.14.0",
-    "typescript": "^5.5.4"
+    "typescript": "^5.5.4",
+    "zod": "^3.23.8"
   }
 }
diff --git a/src/rules/no-named-as-default.ts b/src/rules/no-named-as-default.ts
index 7d6697a4..673f44e8 100644
--- a/src/rules/no-named-as-default.ts
+++ b/src/rules/no-named-as-default.ts
@@ -21,44 +21,51 @@ export = createRule<[], MessageId>({
   },
   defaultOptions: [],
   create(context) {
-    function checkDefault(
-      nameKey: 'local' | 'exported',
-      defaultSpecifier: TSESTree.ImportDefaultSpecifier,
-      // | TSESTree.ExportDefaultSpecifier,
-    ) {
-      // #566: default is a valid specifier
-      // @ts-expect-error - ExportDefaultSpecifier is unavailable yet
-      const nameValue = defaultSpecifier[nameKey].name as string
+    function createCheckDefault(nameKey: 'local' | 'exported') {
+      return function checkDefault(
+        defaultSpecifier: TSESTree.ImportDefaultSpecifier,
+        // | TSESTree.ExportDefaultSpecifier,
+      ) {
+        // #566: default is a valid specifier
+        // @ts-expect-error - ExportDefaultSpecifier is unavailable yet
+        const nameValue = defaultSpecifier[nameKey].name as string
 
-      if (nameValue === 'default') {
-        return
-      }
+        if (nameValue === 'default') {
+          return
+        }
 
-      const declaration = importDeclaration(context, defaultSpecifier)
+        const declaration = importDeclaration(context, defaultSpecifier)
 
-      const imports = ExportMap.get(declaration.source.value, context)
-      if (imports == null) {
-        return
-      }
+        const exportMapOfImported = ExportMap.get(
+          declaration.source.value,
+          context,
+        )
+        if (exportMapOfImported == null) {
+          return
+        }
 
-      if (imports.errors.length > 0) {
-        imports.reportErrors(context, declaration)
-        return
-      }
+        if (exportMapOfImported.errors.length > 0) {
+          exportMapOfImported.reportErrors(context, declaration)
+          return
+        }
 
-      if (imports.has('default') && imports.has(nameValue)) {
-        context.report({
-          node: defaultSpecifier,
-          messageId: 'default',
-          data: {
-            name: nameValue,
-          },
-        })
+        if (
+          exportMapOfImported.exports.has('default') &&
+          exportMapOfImported.exports.has(nameValue)
+        ) {
+          context.report({
+            node: defaultSpecifier,
+            messageId: 'default',
+            data: {
+              name: nameValue,
+            },
+          })
+        }
       }
     }
     return {
-      ImportDefaultSpecifier: checkDefault.bind(null, 'local'),
-      ExportDefaultSpecifier: checkDefault.bind(null, 'exported'),
+      ImportDefaultSpecifier: createCheckDefault('local'),
+      ExportDefaultSpecifier: createCheckDefault('exported'),
     }
   },
 })
diff --git a/src/utils/export-map.ts b/src/utils/export-map.ts
index 84e548f5..1b3efb2e 100644
--- a/src/utils/export-map.ts
+++ b/src/utils/export-map.ts
@@ -283,7 +283,7 @@ export class ExportMap {
           return
         }
         case 'ExportAllDeclaration': {
-          m.exports.set(s.exported!.name, n)
+          m.exports.set(getValue(s.exported!), n)
           m.namespace.set(
             getValue(s.exported!),
             addNamespace(exportMeta, s.exported!),
@@ -293,7 +293,7 @@ export class ExportMap {
         }
         case 'ExportSpecifier': {
           if (!('source' in n && n.source)) {
-            m.exports.set(s.exported!.name, n)
+            m.exports.set(getValue(s.exported!), n)
             m.namespace.set(
               getValue(s.exported),
               addNamespace(exportMeta, s.local),
diff --git a/yarn.lock b/yarn.lock
index f9f51603..3eb55d0c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2285,6 +2285,13 @@
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
   integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
 
+"@types/klaw-sync@^6.0.5":
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/@types/klaw-sync/-/klaw-sync-6.0.5.tgz#0a87fa0762673a1a1d2e7b08a51d5d65f0c842cd"
+  integrity sha512-xlavCRyu5ibDjsOc7PSgeUbwOBZdnJsND8gFUVfBilplbBIWhLZVjwtqbZq0327ny3jNt7oviEh5NS9a4LudeQ==
+  dependencies:
+    "@types/node" "*"
+
 "@types/minimist@^1.2.0":
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e"
@@ -7121,3 +7128,8 @@ yocto-queue@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
   integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+zod@^3.23.8:
+  version "3.23.8"
+  resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
+  integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==


Test patch:
diff --git a/test/package.spec.ts b/test/package.spec.ts
index 33c63dc2..9488e479 100644
--- a/test/package.spec.ts
+++ b/test/package.spec.ts
@@ -52,7 +52,6 @@ describe('package', () => {
         continue
       }
       for (const rule of Object.keys(config.rules)) {
-        console.log({ rule, preamble })
         expect(() =>
           require(getRulePath(rule.slice(preamble.length))),
         ).not.toThrow()
diff --git a/test/rules/no-named-as-default.spec.ts b/test/rules/no-named-as-default.spec.ts
index 7e70b266..ec84fa1c 100644
--- a/test/rules/no-named-as-default.spec.ts
+++ b/test/rules/no-named-as-default.spec.ts
@@ -6,17 +6,54 @@ import rule from 'eslint-plugin-import-x/rules/no-named-as-default'
 
 const ruleTester = new TSESLintRuleTester()
 
-console.log({ babel: require(parsers.BABEL) })
-
 ruleTester.run('no-named-as-default', rule, {
   valid: [
+    // https://github.com/un-ts/eslint-plugin-import-x/issues/123
+    test({
+      code: `/** TypeScript */ import klawSync from "klaw-sync";`,
+      settings: {
+        'import-x/extensions': [
+          '.ts',
+          '.cts',
+          '.mts',
+          '.tsx',
+          '.js',
+          '.cjs',
+          '.mjs',
+          '.jsx',
+        ],
+        'import-x/external-module-folders': [
+          'node_modules',
+          'node_modules/@types',
+        ],
+        'import-x/parsers': {
+          '@typescript-eslint/parser': ['.ts', '.cts', '.mts', '.tsx'],
+        },
+        'import-x/resolver': {
+          typescript: true,
+          node: {
+            extensions: [
+              '.ts',
+              '.cts',
+              '.mts',
+              '.tsx',
+              '.js',
+              '.cjs',
+              '.mjs',
+              '.jsx',
+            ],
+          },
+        },
+      },
+    }),
+
     test({
       code: 'import "./malformed.js"',
       languageOptions: { parser: require(parsers.ESPREE) },
     }),
 
-    test({ code: 'import bar, { foo } from "./bar";' }),
-    test({ code: 'import bar, { foo } from "./empty-folder";' }),
+    'import bar, { foo } from "./bar";',
+    'import bar, { foo } from "./empty-folder";',
 
     // es7
     test({
@@ -133,5 +170,15 @@ ruleTester.run('no-named-as-default', rule, {
         parserOptions: { ecmaVersion: 2022 },
       },
     }),
+
+    test({
+      code: `import z from 'zod';`,
+      errors: [
+        {
+          message: "Using exported name 'z' as identifier for default export.",
+          type: 'ImportDefaultSpecifier',
+        },
+      ],
+    }),
   ],
 })
diff --git a/test/utils.ts b/test/utils.ts
index 07c74a85..86e1ba51 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -109,29 +109,29 @@ export function testContext(settings?: PluginSettings) {
  * to crash at runtime
  */
 export const SYNTAX_CASES = [
-  test({ code: 'for (let { foo, bar } of baz) {}' }),
-  test({ code: 'for (let [ foo, bar ] of baz) {}' }),
+  'for (let { foo, bar } of baz) {}',
+  'for (let [ foo, bar ] of baz) {}',
 
-  test({ code: 'const { x, y } = bar' }),
+  'const { x, y } = bar',
   test({
     code: 'const { x, y, ...z } = bar',
     languageOptions: { parser: require(parsers.BABEL) },
   }),
 
   // all the exports
-  test({ code: 'let x; export { x }' }),
-  test({ code: 'let x; export { x as y }' }),
+  'let x; export { x }',
+  'let x; export { x as y }',
 
   // not sure about these since they reference a file
-  // test({ code: 'export { x } from "./y.js"'}),
-  // test({ code: 'export * as y from "./y.js"', languageOptions: { parser: require(parsers.BABEL) } }),
+  // 'export { x } from "./y.js"'}),
+  // 'export * as y from "./y.js"', languageOptions: { parser: require(parsers.BABEL) } }),
 
-  test({ code: 'export const x = null' }),
-  test({ code: 'export var x = null' }),
-  test({ code: 'export let x = null' }),
+  'export const x = null',
+  'export var x = null',
+  'export let x = null',
 
-  test({ code: 'export default x' }),
-  test({ code: 'export default class x {}' }),
+  'export default x',
+  'export default class x {}',
 
   // issue #267: parser opt-in extension list
   test({