Skip to content

Commit e17e073

Browse files
michaelfaithljharb
authored andcommitted
[Fix] no-unused-modules: don't error out when running with flat config and an eslintrc isn't present
This change adjusts how we're instantiating the FileEnumerator from eslint's unsupported api, in the case that the user is running with flat config. We have to turn off the `useEslintrc` property on the ConfigArrayFactory that's passed into the FileEnumerator's constructor. Note: This doesn't fix the fact that the FileEnumerator doesn't have knowledge of what the user's config is ignoring, it just prevents the rule from looking for a legacy / rc config and erroring out. FileEnumerator used the rc config to understand which files to ignore.
1 parent accab53 commit e17e073

File tree

14 files changed

+156
-116
lines changed

14 files changed

+156
-116
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1010
- add [`enforce-node-protocol-usage`] rule and `import/node-version` setting ([#3024], thanks [@GoldStrikeArch] and [@sevenc-nanashi])
1111
- add TypeScript types ([#3097], thanks [@G-Rath])
1212

13+
### Fixed
14+
- [`no-unused-modules`]: don't error out when running with flat config and an eslintrc isn't present ([#3116], thanks [@michaelfaith])
15+
1316
### Changed
1417
- [Docs] [`extensions`], [`order`]: improve documentation ([#3106], thanks [@Xunnamius])
1518

@@ -1161,6 +1164,7 @@ for info on changes for earlier releases.
11611164

11621165
[`memo-parser`]: ./memo-parser/README.md
11631166

1167+
[#3116]: https://github.com/import-js/eslint-plugin-import/pull/3116
11641168
[#3106]: https://github.com/import-js/eslint-plugin-import/pull/3106
11651169
[#3097]: https://github.com/import-js/eslint-plugin-import/pull/3097
11661170
[#3073]: https://github.com/import-js/eslint-plugin-import/pull/3073

examples/flat/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"cross-env": "^7.0.3",
1313
"eslint": "^8.57.0",
1414
"eslint-plugin-import": "file:../..",
15+
"move-file-cli": "^3.0.0",
1516
"typescript": "^5.4.5"
1617
}
1718
}

examples/v9/eslint.config.mjs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import importPlugin from 'eslint-plugin-import';
2+
import js from '@eslint/js';
3+
import tsParser from '@typescript-eslint/parser';
4+
5+
export default [
6+
js.configs.recommended,
7+
importPlugin.flatConfigs.recommended,
8+
importPlugin.flatConfigs.react,
9+
importPlugin.flatConfigs.typescript,
10+
{
11+
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
12+
languageOptions: {
13+
parser: tsParser,
14+
ecmaVersion: 'latest',
15+
sourceType: 'module',
16+
},
17+
ignores: ['eslint.config.mjs', '**/exports-unused.ts'],
18+
rules: {
19+
'no-unused-vars': 'off',
20+
'import/no-dynamic-require': 'warn',
21+
'import/no-nodejs-modules': 'warn',
22+
'import/no-unused-modules': ['warn', { unusedExports: true }],
23+
'import/no-cycle': 'warn',
24+
},
25+
},
26+
];

examples/v9/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "v9",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"lint": "eslint src --report-unused-disable-directives"
7+
},
8+
"devDependencies": {
9+
"@eslint/js": "^9.17.0",
10+
"@types/node": "^20.14.5",
11+
"@typescript-eslint/parser": "^8.18.0",
12+
"cross-env": "^7.0.3",
13+
"eslint": "^9.17.0",
14+
"eslint-plugin-import": "file:../..",
15+
"move-file-cli": "^3.0.0",
16+
"typescript": "^5.4.5"
17+
}
18+
}

examples/v9/src/depth-zero.js

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();
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function foo() {}
2+
3+
export const bar = () => import("../depth-zero").then(({foo}) => foo);

examples/v9/src/exports-unused.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export type ScalarType = string | number;
2+
export type ObjType = {
3+
a: ScalarType;
4+
b: ScalarType;
5+
};
6+
7+
export const a = 13;
8+
export const b = 18;
9+
10+
const defaultExport: ObjType = { a, b };
11+
12+
export default defaultExport;

examples/v9/src/exports.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export type ScalarType = string | number;
2+
export type ObjType = {
3+
a: ScalarType;
4+
b: ScalarType;
5+
};
6+
7+
export const a = 13;
8+
export const b = 18;
9+
10+
const defaultExport: ObjType = { a, b };
11+
12+
export default defaultExport;

examples/v9/src/imports.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//import c from './exports';
2+
import { a, b } from './exports';
3+
import type { ScalarType, ObjType } from './exports';
4+
5+
import path from 'path';
6+
import fs from 'node:fs';
7+
import console from 'console';

examples/v9/src/jsx.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const Components = () => {
2+
return <></>;
3+
};

examples/v9/tsconfig.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"jsx": "react-jsx",
4+
"lib": ["ESNext"],
5+
"target": "ESNext",
6+
"module": "ESNext",
7+
"rootDir": "./",
8+
"moduleResolution": "Bundler",
9+
"esModuleInterop": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"strict": true,
12+
"skipLibCheck": true
13+
}
14+
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@
3333
"test": "npm run tests-only",
3434
"test-compiled": "npm run prepublish && BABEL_ENV=testCompiled mocha --compilers js:babel-register tests/src",
3535
"test-all": "node --require babel-register ./scripts/testAll",
36-
"test-examples": "npm run build && npm run test-example:legacy && npm run test-example:flat",
36+
"test-examples": "npm run build && npm run test-example:legacy && npm run test-example:flat && npm run test-example:v9",
3737
"test-example:legacy": "cd examples/legacy && npm install && npm run lint",
3838
"test-example:flat": "cd examples/flat && npm install && npm run lint",
39+
"test-example:v9": "cd examples/v9 && npm install && npm run lint",
3940
"test-types": "npx --package typescript@latest tsc --noEmit index.d.ts",
4041
"prepublishOnly": "safe-publish-latest && npm run build",
4142
"prepublish": "not-in-publish || npm run prepublishOnly",

src/core/fsWalk.js

Lines changed: 0 additions & 48 deletions
This file was deleted.

src/rules/no-unused-modules.js

Lines changed: 51 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ import { getPhysicalFilename } from 'eslint-module-utils/contextCompat';
88
import { getFileExtensions } from 'eslint-module-utils/ignore';
99
import resolve from 'eslint-module-utils/resolve';
1010
import visit from 'eslint-module-utils/visit';
11-
import { dirname, join, resolve as resolvePath } from 'path';
11+
import { dirname, join } from 'path';
1212
import readPkgUp from 'eslint-module-utils/readPkgUp';
1313
import values from 'object.values';
1414
import includes from 'array-includes';
1515
import flatMap from 'array.prototype.flatmap';
1616

17-
import { walkSync } from '../core/fsWalk';
1817
import ExportMapBuilder from '../exportMap/builder';
1918
import recursivePatternCapture from '../exportMap/patternCapture';
2019
import docsUrl from '../docsUrl';
@@ -51,21 +50,62 @@ function requireFileEnumerator() {
5150
}
5251

5352
/**
54-
*
53+
* Given a FileEnumerator class, instantiate and load the list of files.
5554
* @param FileEnumerator the `FileEnumerator` class from `eslint`'s internal api
5655
* @param {string} src path to the src root
5756
* @param {string[]} extensions list of supported extensions
5857
* @returns {{ filename: string, ignored: boolean }[]} list of files to operate on
5958
*/
6059
function listFilesUsingFileEnumerator(FileEnumerator, src, extensions) {
61-
const e = new FileEnumerator({
60+
// We need to know whether this is being run with flat config in order to
61+
// determine how to report errors if FileEnumerator throws due to a lack of eslintrc.
62+
63+
const { ESLINT_USE_FLAT_CONFIG } = process.env;
64+
65+
// This condition is sufficient to test in v8, since the environment variable is necessary to turn on flat config
66+
let isUsingFlatConfig = ESLINT_USE_FLAT_CONFIG && process.env.ESLINT_USE_FLAT_CONFIG !== 'false';
67+
68+
// In the case of using v9, we can check the `shouldUseFlatConfig` function
69+
// If this function is present, then we assume it's v9
70+
try {
71+
const { shouldUseFlatConfig } = require('eslint/use-at-your-own-risk');
72+
isUsingFlatConfig = shouldUseFlatConfig && ESLINT_USE_FLAT_CONFIG !== 'false';
73+
} catch (_) {
74+
// We don't want to throw here, since we only want to update the
75+
// boolean if the function is available.
76+
}
77+
78+
const enumerator = new FileEnumerator({
6279
extensions,
6380
});
6481

65-
return Array.from(
66-
e.iterateFiles(src),
67-
({ filePath, ignored }) => ({ filename: filePath, ignored }),
68-
);
82+
try {
83+
return Array.from(
84+
enumerator.iterateFiles(src),
85+
({ filePath, ignored }) => ({ filename: filePath, ignored }),
86+
);
87+
} catch (e) {
88+
// If we're using flat config, and FileEnumerator throws due to a lack of eslintrc,
89+
// then we want to throw an error so that the user knows about this rule's reliance on
90+
// the legacy config.
91+
if (
92+
isUsingFlatConfig
93+
&& e.message.includes('No ESLint configuration found')
94+
) {
95+
throw new Error(`
96+
Due to the exclusion of certain internal ESLint APIs when using flat config,
97+
the import/no-unused-modules rule requires an .eslintrc file to know which
98+
files to ignore (even when using flat config).
99+
The .eslintrc file only needs to contain "ignorePatterns", or can be empty if
100+
you do not want to ignore any files.
101+
102+
See https://github.com/import-js/eslint-plugin-import/issues/3079
103+
for additional context.
104+
`);
105+
}
106+
// If this isn't the case, then we'll just let the error bubble up
107+
throw e;
108+
}
69109
}
70110

71111
/**
@@ -107,70 +147,14 @@ function listFilesWithLegacyFunctions(src, extensions) {
107147
}
108148
}
109149

110-
/**
111-
* Given a source root and list of supported extensions, use fsWalk and the
112-
* new `eslint` `context.session` api to build the list of files we want to operate on
113-
* @param {string[]} srcPaths array of source paths (for flat config this should just be a singular root (e.g. cwd))
114-
* @param {string[]} extensions list of supported extensions
115-
* @param {{ isDirectoryIgnored: (path: string) => boolean, isFileIgnored: (path: string) => boolean }} session eslint context session object
116-
* @returns {string[]} list of files to operate on
117-
*/
118-
function listFilesWithModernApi(srcPaths, extensions, session) {
119-
/** @type {string[]} */
120-
const files = [];
121-
122-
for (let i = 0; i < srcPaths.length; i++) {
123-
const src = srcPaths[i];
124-
// Use walkSync along with the new session api to gather the list of files
125-
const entries = walkSync(src, {
126-
deepFilter(entry) {
127-
const fullEntryPath = resolvePath(src, entry.path);
128-
129-
// Include the directory if it's not marked as ignore by eslint
130-
return !session.isDirectoryIgnored(fullEntryPath);
131-
},
132-
entryFilter(entry) {
133-
const fullEntryPath = resolvePath(src, entry.path);
134-
135-
// Include the file if it's not marked as ignore by eslint and its extension is included in our list
136-
return (
137-
!session.isFileIgnored(fullEntryPath)
138-
&& extensions.find((extension) => entry.path.endsWith(extension))
139-
);
140-
},
141-
});
142-
143-
// Filter out directories and map entries to their paths
144-
files.push(
145-
...entries
146-
.filter((entry) => !entry.dirent.isDirectory())
147-
.map((entry) => entry.path),
148-
);
149-
}
150-
return files;
151-
}
152-
153150
/**
154151
* Given a src pattern and list of supported extensions, return a list of files to process
155152
* with this rule.
156153
* @param {string} src - file, directory, or glob pattern of files to act on
157154
* @param {string[]} extensions - list of supported file extensions
158-
* @param {import('eslint').Rule.RuleContext} context - the eslint context object
159155
* @returns {string[] | { filename: string, ignored: boolean }[]} the list of files that this rule will evaluate.
160156
*/
161-
function listFilesToProcess(src, extensions, context) {
162-
// If the context object has the new session functions, then prefer those
163-
// Otherwise, fallback to using the deprecated `FileEnumerator` for legacy support.
164-
// https://github.com/eslint/eslint/issues/18087
165-
if (
166-
context.session
167-
&& context.session.isFileIgnored
168-
&& context.session.isDirectoryIgnored
169-
) {
170-
return listFilesWithModernApi(src, extensions, context.session);
171-
}
172-
173-
// Fallback to og FileEnumerator
157+
function listFilesToProcess(src, extensions) {
174158
const FileEnumerator = requireFileEnumerator();
175159

176160
// If we got the FileEnumerator, then let's go with that
@@ -295,10 +279,10 @@ const isNodeModule = (path) => (/\/(node_modules)\//).test(path);
295279
function resolveFiles(src, ignoreExports, context) {
296280
const extensions = Array.from(getFileExtensions(context.settings));
297281

298-
const srcFileList = listFilesToProcess(src, extensions, context);
282+
const srcFileList = listFilesToProcess(src, extensions);
299283

300284
// prepare list of ignored files
301-
const ignoredFilesList = listFilesToProcess(ignoreExports, extensions, context);
285+
const ignoredFilesList = listFilesToProcess(ignoreExports, extensions);
302286

303287
// The modern api will return a list of file paths, rather than an object
304288
if (ignoredFilesList.length && typeof ignoredFilesList[0] === 'string') {

0 commit comments

Comments
 (0)