Repository: un-ts/eslint-plugin-import-x
Pull request #64 (2024-03-24 15:13:06)

fix(export-map): incorrect internal namespaces info

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: bb3c39cc8cf72e2f6bf73a11ad25fcab5dc39dcf

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                   | Type  |
| ---------------------- | ----- |
| eslint-plugin-import-x | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/un-ts/eslint-plugin-import-x/new/fix/export-map?filename=.changeset/slow-masks-try.md&value=---%0A%22eslint-plugin-import-x%22%3A%20patch%0A---%0A%0Afix(export-map)%3A%20incorrect%20internal%20namespaces%20info%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/64/builds/487061) or the icon next to each commit SHA.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/pull/64?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts) Report
Attention: Patch coverage is `93.10345%` with `2 lines` in your changes are missing coverage. Please review.
> Project coverage is 95.93%. Comparing base [(`2eca701`)](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/commit/2eca701cc02801023c6d354fe726e47fee2b3c01?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts) to head [(`bb3c39c`)](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/pull/64?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts).

| [Files](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/pull/64?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts) | Patch % | Lines |
|---|---|---|
| [src/utils/get-value.ts](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/pull/64?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts#diff-c3JjL3V0aWxzL2dldC12YWx1ZS50cw==) | 80.00% | [2 Missing :warning: ](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/pull/64?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master      #64      +/-   ##
==========================================
- Coverage   96.48%   95.93%   -0.55%     
==========================================
  Files          92       93       +1     
  Lines        4432     4427       -5     
  Branches     1509     1493      -16     
==========================================
- Hits         4276     4247      -29     
- Misses        150      170      +20     
- Partials        6       10       +4     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/pull/64?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts).


Patch:
diff --git a/.changeset/stale-sheep-yawn.md b/.changeset/stale-sheep-yawn.md
new file mode 100644
index 00000000..418174e8
--- /dev/null
+++ b/.changeset/stale-sheep-yawn.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+fix(export-map): incorrect internal `namespaces` info
diff --git a/src/rules/export.ts b/src/rules/export.ts
index 412a632e..82492c4c 100644
--- a/src/rules/export.ts
+++ b/src/rules/export.ts
@@ -1,6 +1,11 @@
 import type { TSESTree } from '@typescript-eslint/utils'
 
-import { ExportMap, recursivePatternCapture, createRule } from '../utils'
+import {
+  ExportMap,
+  recursivePatternCapture,
+  createRule,
+  getValue,
+} from '../utils'
 
 /*
 Notes on TypeScript namespaces aka TSModuleDeclaration:
@@ -181,9 +186,7 @@ export = createRule<[], MessageId>({
 
       ExportSpecifier(node) {
         addNamed(
-          node.exported.name ||
-            // @ts-expect-error - legacy parser type
-            node.exported.value,
+          getValue(node.exported),
           node.exported,
           getParent(node.parent!),
         )
diff --git a/src/rules/namespace.ts b/src/rules/namespace.ts
index 0444948e..c3d4ad4a 100644
--- a/src/rules/namespace.ts
+++ b/src/rules/namespace.ts
@@ -6,6 +6,7 @@ import {
   ExportMap,
   createRule,
   declaredScope,
+  getValue,
 } from '../utils'
 
 type MessageId =
@@ -62,9 +63,7 @@ function processBodyStatement(
       case 'ImportSpecifier': {
         const meta = imports.get(
           'imported' in specifier && specifier.imported
-            ? specifier.imported.name ||
-                // @ts-expect-error - legacy parser node
-                specifier.imported.value
+            ? getValue(specifier.imported)
             : // default to 'default' for default
               'default',
         )
diff --git a/src/rules/no-default-export.ts b/src/rules/no-default-export.ts
index f102ae35..c7aa4140 100644
--- a/src/rules/no-default-export.ts
+++ b/src/rules/no-default-export.ts
@@ -42,7 +42,7 @@ export = createRule({
             'default',
         )) {
           const { loc } = getSourceCode(context).getFirstTokens(node)[1] || {}
-          // @ts-expect-error - legacy parser type
+          // @ts-expect-error - experimental parser type
           if (specifier.type === 'ExportDefaultSpecifier') {
             context.report({
               node,
diff --git a/src/rules/no-named-default.ts b/src/rules/no-named-default.ts
index 21738bb4..122a90f7 100644
--- a/src/rules/no-named-default.ts
+++ b/src/rules/no-named-default.ts
@@ -1,4 +1,4 @@
-import { createRule } from '../utils'
+import { createRule, getValue } from '../utils'
 
 type MessageId = 'default'
 
@@ -31,9 +31,7 @@ export = createRule<[], MessageId>({
 
           if (
             im.type === 'ImportSpecifier' &&
-            (im.imported.name ||
-              // @ts-expect-error - legacy parser type
-              im.imported.value) === 'default'
+            getValue(im.imported) === 'default'
           ) {
             context.report({
               node: im.local,
diff --git a/src/rules/no-unused-modules.ts b/src/rules/no-unused-modules.ts
index 38a9faad..86a7b217 100644
--- a/src/rules/no-unused-modules.ts
+++ b/src/rules/no-unused-modules.ts
@@ -17,6 +17,7 @@ import {
   getFileExtensions,
   readPkgUp,
   visit,
+  getValue,
 } from '../utils'
 
 function listFilesToProcess(src: string[], extensions: FileExtension[]) {
@@ -633,11 +634,7 @@ export = createRule<Options[], MessageId>({
           if (s.specifiers.length > 0) {
             for (const specifier of s.specifiers) {
               if (specifier.exported) {
-                newExportIdentifiers.add(
-                  specifier.exported.name ||
-                    // @ts-expect-error - legacy parser type
-                    specifier.exported.value,
-                )
+                newExportIdentifiers.add(getValue(specifier.exported))
               }
             }
           }
@@ -753,10 +750,7 @@ export = createRule<Options[], MessageId>({
             context,
           )
           for (const specifier of astNode.specifiers) {
-            const name =
-              specifier.local.name ||
-              // @ts-expect-error - legacy parser type
-              specifier.local.value
+            const name = getValue(specifier.local)
             if (name === DEFAULT) {
               newDefaultImports.add(resolvedPath!)
             } else {
@@ -800,12 +794,7 @@ export = createRule<Options[], MessageId>({
               specifier.type !== AST_NODE_TYPES.ImportNamespaceSpecifier,
           )) {
             if ('imported' in specifier) {
-              newImports.set(
-                specifier.imported.name ||
-                  // @ts-expect-error - legacy parser type
-                  specifier.imported.value,
-                resolvedPath,
-              )
+              newImports.set(getValue(specifier.imported), resolvedPath)
             }
           }
         }
@@ -1004,12 +993,7 @@ export = createRule<Options[], MessageId>({
       },
       ExportNamedDeclaration(node) {
         for (const specifier of node.specifiers) {
-          checkUsage(
-            specifier,
-            specifier.exported.name ||
-              // @ts-expect-error - legacy parser type
-              specifier.exported.value,
-          )
+          checkUsage(specifier, getValue(specifier.exported))
         }
         forEachDeclarationIdentifier(node.declaration, name => {
           checkUsage(node, name)
diff --git a/src/utils/export-map.ts b/src/utils/export-map.ts
index 2427f6e6..526dd6d5 100644
--- a/src/utils/export-map.ts
+++ b/src/utils/export-map.ts
@@ -19,6 +19,7 @@ import type {
   RuleContext,
 } from '../types'
 
+import { getValue } from './get-value'
 import { hashObject } from './hash'
 import { hasValidExtension, ignore } from './ignore'
 import { parse } from './parse'
@@ -212,7 +213,7 @@ export class ExportMap {
       })
     }
 
-    const namespaces = new Map()
+    const namespaces = new Map</* identifier */ string, /* source */ string>()
 
     function remotePath(value: string) {
       return relative(value, filepath, context.settings)
@@ -226,25 +227,18 @@ export class ExportMap {
       return ExportMap.for(childContext(rp, context))
     }
 
-    function getNamespace(identifier: TSESTree.Identifier | string) {
-      const namespace =
-        typeof identifier === 'string' ? identifier : identifier.name
+    function getNamespace(namespace: string) {
       if (!namespaces.has(namespace)) {
         return
       }
 
       return function () {
-        return resolveImport(namespaces.get(namespace))
+        return resolveImport(namespaces.get(namespace)!)
       }
     }
 
-    function addNamespace(
-      object: object,
-      identifier: TSESTree.Identifier | string,
-    ) {
-      const namespace =
-        typeof identifier === 'string' ? identifier : identifier.name
-      const nsfn = getNamespace(namespace)
+    function addNamespace(object: object, identifier: TSESTree.Identifier) {
+      const nsfn = getNamespace(getValue(identifier))
       if (nsfn) {
         Object.defineProperty(object, 'namespace', { get: nsfn })
       }
@@ -289,19 +283,16 @@ export class ExportMap {
         }
         case 'ExportAllDeclaration': {
           m.namespace.set(
-            s.exported!.name ||
-              // @ts-expect-error - legacy parser type
-              s.exported!.value,
-            addNamespace(exportMeta, s.source.value),
+            getValue(s.exported!),
+            addNamespace(exportMeta, s.exported!),
           )
+
           return
         }
         case 'ExportSpecifier': {
           if (!('source' in n && n.source)) {
             m.namespace.set(
-              s.exported.name ||
-                // @ts-expect-error - legacy parser type
-                s.exported.value,
+              getValue(s.exported),
               addNamespace(exportMeta, s.local),
             )
             return
@@ -342,11 +333,7 @@ export class ExportMap {
       const importedSpecifiers = new Set<string>()
       for (const specifier of n.specifiers) {
         if (specifier.type === 'ImportSpecifier') {
-          importedSpecifiers.add(
-            specifier.imported.name ||
-              // @ts-expect-error - legacy parser type
-              specifier.imported.value,
-          )
+          importedSpecifiers.add(getValue(specifier.imported))
         } else if (supportedImportTypes.has(specifier.type)) {
           importedSpecifiers.add(specifier.type)
         }
@@ -460,6 +447,7 @@ export class ExportMap {
           m.dependencies.add(getter)
         }
         if (n.exported) {
+          namespaces.set(n.exported.name, n.source.value)
           processSpecifier(n, n.exported, m)
         }
         continue
@@ -520,7 +508,9 @@ export class ExportMap {
           }
         }
 
-        for (const s of n.specifiers) processSpecifier(s, n, m)
+        for (const s of n.specifiers) {
+          processSpecifier(s, n, m)
+        }
       }
 
       const exports = ['TSExportAssignment']
diff --git a/src/utils/get-value.ts b/src/utils/get-value.ts
new file mode 100644
index 00000000..70efd501
--- /dev/null
+++ b/src/utils/get-value.ts
@@ -0,0 +1,17 @@
+import { TSESTree } from '@typescript-eslint/utils'
+
+export const getValue = (
+  node: TSESTree.Identifier | TSESTree.StringLiteral,
+) => {
+  switch (node.type) {
+    case TSESTree.AST_NODE_TYPES.Identifier: {
+      return node.name
+    }
+    case TSESTree.AST_NODE_TYPES.Literal: {
+      return node.value
+    }
+    default: {
+      throw new Error(`Unsupported node type: ${(node as TSESTree.Node).type}`)
+    }
+  }
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 5bf80168..2fc66084 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -5,6 +5,7 @@ export * from './docs-url'
 export * from './export-map'
 export * from './get-ancestors'
 export * from './get-scope'
+export * from './get-value'
 export * from './hash'
 export * from './ignore'
 export * from './import-declaration'


Test patch:
diff --git a/test/utils/export-map.spec.ts b/test/utils/export-map.spec.ts
index 60c6f52f..3dab1798 100644
--- a/test/utils/export-map.spec.ts
+++ b/test/utils/export-map.spec.ts
@@ -107,10 +107,14 @@ describe('ExportMap', () => {
           .readFileSync(path, { encoding: 'utf8' })
           .replaceAll(/\r?\n/g, lineEnding)
 
-        const imports = ExportMap.parse(path, contents, parseContext)!
+        let imports: ExportMap
 
-        // sanity checks
-        expect(imports.errors).toHaveLength(0)
+        beforeAll(() => {
+          imports = ExportMap.parse(path, contents, parseContext)!
+
+          // sanity checks
+          expect(imports.errors).toHaveLength(0)
+        })
 
         it('works with named imports.', () => {
           expect(imports.has('fn')).toBe(true)
@@ -184,10 +188,15 @@ describe('ExportMap', () => {
       describe('full module', () => {
         const path = testFilePath('deprecated-file.js')
         const contents = fs.readFileSync(path, { encoding: 'utf8' })
-        const imports = ExportMap.parse(path, contents, parseContext)!
 
-        // sanity checks
-        expect(imports.errors).toHaveLength(0)
+        let imports: ExportMap
+
+        beforeAll(() => {
+          imports = ExportMap.parse(path, contents, parseContext)!
+
+          // sanity checks
+          expect(imports.errors).toHaveLength(0)
+        })
 
         it('has JSDoc metadata', () => {
           expect(imports.doc).toBeDefined()
@@ -281,8 +290,7 @@ describe('ExportMap', () => {
       expect(def.get('default')!.namespace!.has('c')).toBe(true)
     })
 
-    // FIXME: check and enable
-    it.skip('works with @babel/eslint-parser & ES7 namespace exports', function () {
+    it('works with @babel/eslint-parser & ES7 namespace exports', function () {
       const path = testFilePath('deep-es7/a.js')
       const contents = fs.readFileSync(path, { encoding: 'utf8' })
       const a = ExportMap.parse(path, contents, babelContext)!
@@ -376,10 +384,8 @@ describe('ExportMap', () => {
       settings: { 'import-x/extensions': ['.js'] as const },
     }
 
-    const imports = ExportMap.get('./typescript.ts', context)
-
     it('returns nothing for a TypeScript file', () => {
-      expect(imports).toBeFalsy()
+      expect(ExportMap.get('./typescript.ts', context)).toBeFalsy()
     })
   })
 
@@ -395,9 +401,17 @@ describe('ExportMap', () => {
 
       jest.setTimeout(20e3) // takes a long time :shrug:
 
-      const spied = jest.spyOn(getTsconfig, 'getTsconfig').mockClear()
+      const spied = jest.spyOn(getTsconfig, 'getTsconfig')
+
+      let imports: ExportMap
 
-      const imports = ExportMap.get('./typescript.ts', context)!
+      beforeAll(() => {
+        imports = ExportMap.get('./typescript.ts', context)!
+      })
+
+      beforeEach(() => {
+        spied.mockClear()
+      })
 
       afterAll(() => {
         spied.mockRestore()