Skip to content

Commit 3a3f5f8

Browse files
authored
fix: try/catch enumerator.iterateFiles gracefully (#360)
1 parent dfcbe2c commit 3a3f5f8

File tree

14 files changed

+180
-166
lines changed

14 files changed

+180
-166
lines changed

.changeset/curvy-swans-ring.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-import-x": patch
3+
---
4+
5+
fix: try/catch `enumerator.iterateFiles` gracefully

.eslintrc.cjs

Lines changed: 1 addition & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,155 +1,3 @@
11
// ! This file is here for testing `no-unused-modules` rule for eslintrc
22

3-
const { version } = require('eslint/package.json')
4-
5-
const noEslintrc = +version.split('.')[0] > 8
6-
7-
const testCompiled = process.env.TEST_COMPILED === '1'
8-
9-
/** @type {import('eslint').Linter.Config} */
10-
module.exports = {
11-
root: true,
12-
reportUnusedDisableDirectives: true,
13-
extends: [
14-
'eslint:recommended',
15-
'plugin:@typescript-eslint/recommended',
16-
'plugin:eslint-plugin/recommended',
17-
testCompiled && 'plugin:import-x/recommended',
18-
'plugin:json/recommended-legacy',
19-
'plugin:mdx/recommended',
20-
'plugin:n/recommended',
21-
!noEslintrc && 'plugin:unicorn/recommended',
22-
'plugin:yml/standard',
23-
'plugin:yml/prettier',
24-
'plugin:prettier/recommended',
25-
].filter(Boolean),
26-
env: {
27-
node: true,
28-
es6: true,
29-
es2017: true,
30-
},
31-
parserOptions: {
32-
sourceType: 'module',
33-
ecmaVersion: 2020,
34-
},
35-
rules: {
36-
'@typescript-eslint/no-non-null-assertion': 'off',
37-
'@typescript-eslint/no-require-imports': 'off',
38-
39-
'no-constant-condition': noEslintrc ? 'error' : 'off',
40-
41-
'eslint-plugin/consistent-output': ['error', 'always'],
42-
'eslint-plugin/meta-property-ordering': 'error',
43-
'eslint-plugin/no-deprecated-context-methods': 'error',
44-
'eslint-plugin/no-deprecated-report-api': 'off',
45-
'eslint-plugin/prefer-replace-text': 'error',
46-
'eslint-plugin/report-message-format': 'error',
47-
'eslint-plugin/require-meta-docs-description': [
48-
'error',
49-
{ pattern: String.raw`^(Enforce|Ensure|Prefer|Forbid).+\.$` },
50-
],
51-
'eslint-plugin/require-meta-schema': 'error',
52-
'eslint-plugin/require-meta-type': 'error',
53-
'n/no-extraneous-require': 'off',
54-
'n/no-missing-import': 'off',
55-
'n/no-missing-require': 'off',
56-
'n/no-unsupported-features/es-syntax': 'off',
57-
...(noEslintrc || {
58-
'unicorn/filename-case': [
59-
'error',
60-
{
61-
case: 'kebabCase',
62-
ignore: [String.raw`^(CONTRIBUTING|README)\.md$`],
63-
},
64-
],
65-
}),
66-
'unicorn/no-array-callback-reference': 'off',
67-
'unicorn/no-array-reduce': 'off',
68-
'unicorn/no-null': 'off',
69-
'unicorn/prefer-module': 'off',
70-
'unicorn/prevent-abbreviations': 'off',
71-
'unicorn/prefer-at': 'off',
72-
'unicorn/prefer-export-from': ['error', { ignoreUsedVariables: true }],
73-
74-
// dog fooding
75-
...(testCompiled && {
76-
'import-x/no-extraneous-dependencies': [
77-
'error',
78-
{
79-
devDependencies: ['test/**'],
80-
optionalDependencies: false,
81-
peerDependencies: true,
82-
bundledDependencies: false,
83-
},
84-
],
85-
'import-x/unambiguous': 'off',
86-
}),
87-
},
88-
89-
overrides: [
90-
{
91-
files: ['*.ts'],
92-
excludedFiles: ['test/fixtures'],
93-
rules: {
94-
'@typescript-eslint/array-type': [
95-
2,
96-
{
97-
default: 'array-simple',
98-
},
99-
],
100-
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
101-
'@typescript-eslint/consistent-type-imports': [
102-
'error',
103-
{
104-
fixStyle: 'inline-type-imports',
105-
},
106-
],
107-
'@typescript-eslint/no-unused-vars': [
108-
'error',
109-
{
110-
argsIgnorePattern: '^_',
111-
varsIgnorePattern: '^_',
112-
},
113-
],
114-
...(testCompiled && {
115-
'import-x/consistent-type-specifier-style': 'error',
116-
'import-x/order': [
117-
'error',
118-
{
119-
alphabetize: {
120-
order: 'asc',
121-
},
122-
'newlines-between': 'always',
123-
},
124-
],
125-
}),
126-
},
127-
settings: {
128-
'import-x/resolver': {
129-
typescript: {
130-
project: 'tsconfig.base.json',
131-
},
132-
},
133-
},
134-
},
135-
{
136-
files: 'test/**',
137-
env: {
138-
jest: true,
139-
},
140-
},
141-
{
142-
files: 'global.d.ts',
143-
rules: {
144-
'import-x/no-extraneous-dependencies': 'off',
145-
},
146-
},
147-
{
148-
files: 'README.md',
149-
rules: {
150-
// https://github.com/bmish/eslint-doc-generator/issues/655
151-
'no-irregular-whitespace': 'off',
152-
},
153-
},
154-
],
155-
}
3+
module.exports = {}

docs/rules/no-unused-modules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Reports:
99
- dynamic imports are supported if argument is a literal string
1010

1111
> [!IMPORTANT]
12-
> This rule is only compatible with legacy configs. If you make use of the new flat config, either do not enable this rule, or provide a legacy eslintrc config at the same time together for this rule to be functional.
12+
> This rule is only compatible with legacy configs. If you make use of the new flat config, either do not enable this rule, or provide a legacy eslintrc config (even empty) at the same time together for this rule to be functional.
1313
1414
## Rule Details
1515

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
"@types/klaw-sync": "^6.0.5",
120120
"@types/node": "^22.15.27",
121121
"@types/pnpapi": "^0.0.5",
122+
"@types/tmp": "^0.2.6",
122123
"@typescript-eslint/eslint-plugin": "^8.33.0",
123124
"@typescript-eslint/parser": "^8.33.0",
124125
"@typescript-eslint/rule-tester": "^8.33.0",
@@ -153,6 +154,7 @@
153154
"redux": "^5.0.1",
154155
"simple-git-hooks": "^2.13.0",
155156
"tinyexec": "^1.0.1",
157+
"tmp": "^0.2.3",
156158
"ts-node": "^10.9.2",
157159
"tsdown": "^0.12.5",
158160
"type-fest": "^4.41.0",

src/rules/no-unused-modules.ts

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,62 @@ import {
2323
} from '../utils/index.js'
2424

2525
// eslint-disable-next-line import-x/no-named-as-default-member -- incorrect types , commonjs actually
26-
const { FileEnumerator } = eslintUnsupportedApi
26+
const { FileEnumerator, shouldUseFlatConfig } = eslintUnsupportedApi
2727

28-
function listFilesToProcess(src: string[], extensions: FileExtension[]) {
29-
const enumerator = new FileEnumerator({
30-
extensions,
31-
})
28+
function listFilesUsingFileEnumerator(
29+
src: string[],
30+
extensions: FileExtension[],
31+
) {
32+
// We need to know whether this is being run with flat config in order to
33+
// determine how to report errors if FileEnumerator throws due to a lack of eslintrc.
34+
35+
const { ESLINT_USE_FLAT_CONFIG } = process.env
36+
37+
// This condition is sufficient to test in v8, since the environment variable is necessary to turn on flat config
38+
let isUsingFlatConfig: boolean
39+
40+
// In the case of using v9, we can check the `shouldUseFlatConfig` function
41+
// If this function is present, then we assume it's v9
42+
try {
43+
isUsingFlatConfig =
44+
// @ts-expect-error -- only available in ESLint v9
45+
shouldUseFlatConfig && ESLINT_USE_FLAT_CONFIG !== 'false'
46+
} catch {
47+
// We don't want to throw here, since we only want to init the
48+
// boolean if the function is available.
49+
isUsingFlatConfig =
50+
!!ESLINT_USE_FLAT_CONFIG && ESLINT_USE_FLAT_CONFIG !== 'false'
51+
}
52+
53+
const enumerator = new FileEnumerator({ extensions })
3254

33-
return Array.from(enumerator.iterateFiles(src), ({ filePath, ignored }) => ({
34-
ignored,
35-
filename: filePath,
36-
}))
55+
try {
56+
return Array.from(
57+
enumerator.iterateFiles(src),
58+
({ filePath, ignored }) => ({ filename: filePath, ignored }),
59+
)
60+
} catch (error) {
61+
// If we're using flat config, and FileEnumerator throws due to a lack of eslintrc,
62+
// then we want to throw an error so that the user knows about this rule's reliance on
63+
// the legacy config.
64+
if (
65+
isUsingFlatConfig &&
66+
(error as Error).message.includes('No ESLint configuration found')
67+
) {
68+
throw new Error(`
69+
Due to the exclusion of certain internal ESLint APIs when using flat config,
70+
the import-x/no-unused-modules rule requires an .eslintrc file (even empty) to know which
71+
files to ignore (even when using flat config).
72+
The .eslintrc file only needs to contain "ignorePatterns", or can be empty if
73+
you do not want to ignore any files.
74+
75+
See https://github.com/import-js/eslint-plugin-import/issues/3079
76+
for additional context.
77+
`)
78+
}
79+
// If this isn't the case, then we'll just let the error bubble up
80+
throw error
81+
}
3782
}
3883

3984
const DEFAULT = 'default'
@@ -141,10 +186,13 @@ const resolveFiles = (
141186
) => {
142187
const extensions = [...getFileExtensions(context.settings)]
143188

144-
const srcFileList = listFilesToProcess(src, extensions)
189+
const srcFileList = listFilesUsingFileEnumerator(src, extensions)
145190

146191
// prepare list of ignored files
147-
const ignoredFilesList = listFilesToProcess(ignoreExports, extensions)
192+
const ignoredFilesList = listFilesUsingFileEnumerator(
193+
ignoreExports,
194+
extensions,
195+
)
148196
for (const { filename } of ignoredFilesList) ignoredFiles.add(filename)
149197

150198
// prepare list of source files, don't consider files from node_modules

test/fixtures/eslint-v9/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

test/fixtures/eslint-v9/.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package-lock=false
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { importX } from 'eslint-plugin-import-x'
2+
import js from '@eslint/js'
3+
4+
export default [
5+
js.configs.recommended,
6+
importX.flatConfigs.recommended,
7+
{
8+
files: ['**/*.{js,mjs,cjs}'],
9+
languageOptions: {
10+
ecmaVersion: 'latest',
11+
sourceType: 'module',
12+
},
13+
ignores: ['eslint.config.mjs', 'node_modules/*'],
14+
rules: {
15+
'no-unused-vars': 'off',
16+
'import-x/no-dynamic-require': 'warn',
17+
'import-x/no-nodejs-modules': 'warn',
18+
'import-x/no-unused-modules': ['warn', { unusedExports: true }],
19+
'import-x/no-cycle': 'warn',
20+
},
21+
},
22+
]

test/fixtures/eslint-v9/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "eslint-v9",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"main": "index.js",
6+
"scripts": {
7+
"lint": "eslint src --report-unused-disable-directives"
8+
},
9+
"devDependencies": {
10+
"@eslint/js": "^9.17.0",
11+
"eslint": "^9.17.0",
12+
"eslint-plugin-import-x": "file:../../.."
13+
}
14+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { foo } from './es6/depth-one-dynamic'
2+
3+
foo()

0 commit comments

Comments
 (0)