Repository: un-ts/eslint-plugin-import-x
Pull request #171 (2024-10-30 11:42:33)

refactor: backport a few upstream changes

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

Latest commit: 9c210e1743143466b9ec64481470fd2f3f8f899b

**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/SukkaW/eslint-plugin-import-x/new/backport-upstream-1?filename=.changeset/spotty-poets-tap.md&value=---%0A%22eslint-plugin-import-x%22%3A%20patch%0A---%0A%0Arefactor%3A%20backport%20a%20few%20upstream%20changes%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/171/builds/555839) or the icon next to each commit SHA.

Patch:
diff --git a/.changeset/fast-carpets-teach.md b/.changeset/fast-carpets-teach.md
new file mode 100644
index 00000000..b7868cd3
--- /dev/null
+++ b/.changeset/fast-carpets-teach.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+Perf: avoid regexp during parser choosing
diff --git a/.changeset/hot-jokes-grab.md b/.changeset/hot-jokes-grab.md
new file mode 100644
index 00000000..6e92068b
--- /dev/null
+++ b/.changeset/hot-jokes-grab.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+Add extra guard for rule `no-named-as-default`. A few guards are borrowed from https://github.com/import-js/eslint-plugin-import/pull/3032, but we don't sync the rest of changes from upstream since we have already implemented a way more performant check.
diff --git a/.changeset/old-years-rest.md b/.changeset/old-years-rest.md
new file mode 100644
index 00000000..9eb51373
--- /dev/null
+++ b/.changeset/old-years-rest.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+More test cases for `no-named-export` and `no-defualt-export` rule specifically with non-module `sourceType`
diff --git a/.changeset/orange-dryers-mix.md b/.changeset/orange-dryers-mix.md
new file mode 100644
index 00000000..e1c7fc1d
--- /dev/null
+++ b/.changeset/orange-dryers-mix.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+Fix `export` when there is only one `TSDeclareFunction` (https://github.com/import-js/eslint-plugin-import/pull/3065)
diff --git a/.changeset/shaggy-mayflies-applaud.md b/.changeset/shaggy-mayflies-applaud.md
new file mode 100644
index 00000000..f263fd3a
--- /dev/null
+++ b/.changeset/shaggy-mayflies-applaud.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+Prevent `ExportMap`'s cache is being tainted by incompatible parser (e.g. old `babel-eslint`). The cache is now skipped w/ incompatible parsers, which might introduce performance impacts only for those who are using incompatible parsers. (https://github.com/import-js/eslint-plugin-import/pull/3062)
diff --git a/.changeset/spicy-mangos-pull.md b/.changeset/spicy-mangos-pull.md
new file mode 100644
index 00000000..397ace3a
--- /dev/null
+++ b/.changeset/spicy-mangos-pull.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+Docs: fix a few typos here and there
diff --git a/.changeset/tough-elephants-divide.md b/.changeset/tough-elephants-divide.md
new file mode 100644
index 00000000..8eb27cc3
--- /dev/null
+++ b/.changeset/tough-elephants-divide.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+Properly fix espree parser w/ ESLint Flat Config
diff --git a/docs/rules/no-relative-packages.md b/docs/rules/no-relative-packages.md
index 8a4efc70..2d990ca5 100644
--- a/docs/rules/no-relative-packages.md
+++ b/docs/rules/no-relative-packages.md
@@ -6,8 +6,7 @@
 
 Use this rule to prevent importing packages through relative paths.
 
-It's useful in Yarn/Lerna workspaces, were it's possible to import a sibling
-package using `../package` relative path, while direct `package` is the correct one.
+It's useful in a monorepo setup, where it's possible to import a sibling package using `../package` relative path, while direct `package` is the correct one.
 
 ## Examples
 
diff --git a/docs/rules/no-restricted-paths.md b/docs/rules/no-restricted-paths.md
index a2f6f015..f199f47c 100644
--- a/docs/rules/no-restricted-paths.md
+++ b/docs/rules/no-restricted-paths.md
@@ -5,7 +5,7 @@
 Some projects contain files which are not always meant to be executed in the same environment.
 For example consider a web application that contains specific code for the server and some specific code for the browser/client. In this case you don’t want to import server-only files in your client code.
 
-In order to prevent such scenarios this rule allows you to define restricted zones where you can forbid files from imported if they match a specific path.
+In order to prevent such scenarios this rule allows you to define restricted zones where you can forbid files from being imported if they match a specific path.
 
 ## Rule Details
 
diff --git a/src/rules/export.ts b/src/rules/export.ts
index 82492c4c..4eb28b42 100644
--- a/src/rules/export.ts
+++ b/src/rules/export.ts
@@ -1,3 +1,4 @@
+import { AST_NODE_TYPES } from '@typescript-eslint/utils'
 import type { TSESTree } from '@typescript-eslint/utils'
 
 import {
@@ -30,37 +31,24 @@ const rootProgram = 'root'
 const tsTypePrefix = 'type:'
 
 /**
- * Detect function overloads like:
+ * Remove function overloads like:
+ *
  * ```ts
  * export function foo(a: number);
  * export function foo(a: string);
  * export function foo(a: number|string) { return a; }
  * ```
  */
-function isTypescriptFunctionOverloads(nodes: Set<TSESTree.Node>) {
-  const nodesArr = [...nodes]
-
-  const idents = nodesArr.flatMap(node =>
-    'declaration' in node && node.declaration?.type === 'TSDeclareFunction'
-      ? node.declaration.id!.name
-      : [],
-  )
-
-  if (new Set(idents).size !== idents.length) {
-    return true
-  }
-
-  const types = new Set(nodesArr.map(node => `${node.parent!.type}` as const))
-  if (!types.has('TSDeclareFunction')) {
-    return false
-  }
-  if (types.size === 1) {
-    return true
-  }
-  if (types.size === 2 && types.has('FunctionDeclaration')) {
-    return true
+function removeTypescriptFunctionOverloads(nodes: Set<TSESTree.Node>) {
+  for (const node of nodes) {
+    const declType =
+      node.type === AST_NODE_TYPES.ExportDefaultDeclaration
+        ? node.declaration.type
+        : node.parent?.type
+    if (declType === AST_NODE_TYPES.TSDeclareFunction) {
+      nodes.delete(node)
+    }
   }
-  return false
 }
 
 /**
@@ -277,14 +265,17 @@ export = createRule<[], MessageId>({
       'Program:exit'() {
         for (const [, named] of namespace) {
           for (const [name, nodes] of named) {
+            if (nodes.size === 0) {
+              continue
+            }
+
+            removeTypescriptFunctionOverloads(nodes)
+
             if (nodes.size <= 1) {
               continue
             }
 
-            if (
-              isTypescriptFunctionOverloads(nodes) ||
-              isTypescriptNamespaceMerging(nodes)
-            ) {
+            if (isTypescriptNamespaceMerging(nodes)) {
               continue
             }
 
diff --git a/src/rules/no-named-as-default.ts b/src/rules/no-named-as-default.ts
index 673f44e8..d2cd1190 100644
--- a/src/rules/no-named-as-default.ts
+++ b/src/rules/no-named-as-default.ts
@@ -49,6 +49,17 @@ export = createRule<[], MessageId>({
           return
         }
 
+        if (!exportMapOfImported.hasDefault) {
+          // The rule is triggered for default imports/exports, so if the imported module has no default
+          // this means we're dealing with incorrect source code anyway
+          return
+        }
+
+        if (!exportMapOfImported.has(nameValue)) {
+          // The name used locally for the default import was not even used in the imported module.
+          return
+        }
+
         if (
           exportMapOfImported.exports.has('default') &&
           exportMapOfImported.exports.has(nameValue)
diff --git a/src/utils/export-map.ts b/src/utils/export-map.ts
index 1b3efb2e..22c74974 100644
--- a/src/utils/export-map.ts
+++ b/src/utils/export-map.ts
@@ -128,7 +128,11 @@ export class ExportMap {
 
     exportMap.mtime = stats().mtime.valueOf()
 
-    exportCache.set(cacheKey, exportMap)
+    // If the visitor keys were not populated, then we shouldn't save anything to the cache,
+    // since the parse results may not be reliable.
+    if (exportMap.visitorKeys) {
+      exportCache.set(cacheKey, exportMap)
+    }
 
     return exportMap
   }
diff --git a/src/utils/parse.ts b/src/utils/parse.ts
index f1ce213a..61b94383 100644
--- a/src/utils/parse.ts
+++ b/src/utils/parse.ts
@@ -29,7 +29,7 @@ function withoutProjectParserOptions(
 const log = debug('eslint-plugin-import-x:parse')
 
 function keysFromParser(
-  parserPath: string | TSESLint.Parser.ParserModule,
+  _parserPath: string | TSESLint.Parser.ParserModule,
   parserInstance: TSESLint.Parser.ParserModule,
   parsedResult?: TSESLint.Parser.ParseResult,
 ) {
@@ -37,8 +37,14 @@ function keysFromParser(
   if (parsedResult && parsedResult.visitorKeys) {
     return parsedResult.visitorKeys
   }
-  if (typeof parserPath === 'string' && /.*espree.*/.test(parserPath)) {
-    // @ts-expect-error - no type yet
+
+  // The espree parser doesn't have the `parseForESLint` function, so we don't ended up with a
+  // `parsedResult` here, but it does expose the visitor keys on the parser instance that we can use.
+  if (
+    parserInstance &&
+    'VisitorKeys' in parserInstance &&
+    parserInstance.VisitorKeys
+  ) {
     return parserInstance.VisitorKeys as TSESLint.SourceCode.VisitorKeys
   }
   return null


Test patch:
diff --git a/test/fixtures/order-redirect-scoped/module/package.json b/test/fixtures/order-redirect-scoped/module/package.json
index 6c4325e8..2ee43c26 100644
--- a/test/fixtures/order-redirect-scoped/module/package.json
+++ b/test/fixtures/order-redirect-scoped/module/package.json
@@ -1,5 +1,5 @@
 {
-  "name": "order-redirect-module",
+  "name": "order-redirect-module-scoped",
   "private": true,
   "main": "../other-module/file.js"
 }
diff --git a/test/rules/export.spec.ts b/test/rules/export.spec.ts
index 10df5bd1..f9ce211e 100644
--- a/test/rules/export.spec.ts
+++ b/test/rules/export.spec.ts
@@ -58,6 +58,14 @@ ruleTester.run('export', rule, {
         }
       `,
     },
+    {
+      code: `
+      export default function foo(param: string): boolean;
+      export default function foo(param: string, param1?: number): boolean {
+        return param && param1;
+      }
+    `,
+    },
   ],
 
   invalid: [
@@ -160,6 +168,15 @@ ruleTester.run('export', rule, {
         },
       },
     }),
+
+    test({
+      code: `
+        export default function a(): void;
+        export default function a() {}
+        export { x as default };
+      `,
+      errors: ['Multiple default exports.', 'Multiple default exports.'],
+    }),
   ],
 })
 
@@ -510,7 +527,7 @@ describe('TypeScript', () => {
       }),
       test({
         code: `
-            export function Foo();
+            export function Foo() { };
             export class Foo { }
             export namespace Foo { }
           `,
@@ -529,7 +546,7 @@ describe('TypeScript', () => {
       test({
         code: `
             export const Foo = 'bar';
-            export function Foo();
+            export function Foo() { };
             export namespace Foo { }
           `,
         errors: [
diff --git a/test/rules/no-cycle.spec.ts b/test/rules/no-cycle.spec.ts
index 3ed48947..3727c94f 100644
--- a/test/rules/no-cycle.spec.ts
+++ b/test/rules/no-cycle.spec.ts
@@ -11,8 +11,8 @@ const error = (message: string) => ({ message })
 
 const test = <T extends ValidTestCase>(def: T) =>
   _test({
-    ...def,
     filename: testFilePath('./cycles/depth-zero.js'),
+    ...def,
   })
 
 ruleTester.run('no-cycle', rule, {
diff --git a/test/rules/no-default-export.spec.ts b/test/rules/no-default-export.spec.ts
index e90d57f8..e38ce7b6 100644
--- a/test/rules/no-default-export.spec.ts
+++ b/test/rules/no-default-export.spec.ts
@@ -8,6 +8,17 @@ const ruleTester = new TSESLintRuleTester()
 
 ruleTester.run('no-default-export', rule, {
   valid: [
+    test({
+      code: 'module.exports = function foo() {}',
+      languageOptions: {
+        parserOptions: {
+          sourceType: 'script',
+        },
+      },
+    }),
+    test({
+      code: 'module.exports = function foo() {}',
+    }),
     test({
       code: `
         export const foo = 'foo';
diff --git a/test/rules/no-named-export.spec.ts b/test/rules/no-named-export.spec.ts
index 444615c2..55829764 100644
--- a/test/rules/no-named-export.spec.ts
+++ b/test/rules/no-named-export.spec.ts
@@ -8,6 +8,17 @@ const ruleTester = new TSESLintRuleTester()
 
 ruleTester.run('no-named-export', rule, {
   valid: [
+    test({
+      code: 'module.export.foo = function () {}',
+    }),
+    test({
+      code: 'module.exports = function foo() {}',
+      languageOptions: {
+        parserOptions: {
+          sourceType: 'script',
+        },
+      },
+    }),
     test({
       code: 'export default function bar() {};',
     }),
diff --git a/test/utils/export-map.spec.ts b/test/utils/export-map.spec.ts
index 7ad26266..c60f77c2 100644
--- a/test/utils/export-map.spec.ts
+++ b/test/utils/export-map.spec.ts
@@ -30,6 +30,17 @@ describe('ExportMap', () => {
     )
   })
 
+  it('does not return a cached copy if the parse does not yield a visitor keys', function () {
+    const mockContext = {
+      ...fakeContext,
+      parserPath: 'not-real',
+    }
+    expect(ExportMap.get('./named-exports', mockContext)).toBeDefined()
+    expect(ExportMap.get('./named-exports', mockContext)).not.toBe(
+      ExportMap.get('./named-exports', mockContext),
+    )
+  })
+
   it('does not return a cached copy after modification', done => {
     const firstAccess = ExportMap.get('./mutator', fakeContext)
     expect(firstAccess).toBeDefined()