Skip to content

Commit bf2d2aa

Browse files
committed
[Fix] no-unused-modules: only honor flat config ignores on ESLint 8.21+, and support older config-array (isIgnored) APIs
Flat config does not exist before ESLint 8.21, so a discovered `eslint.config.js` is inert on older versions and its `ignores` must not filter the file set. Gate the predicate on the installed ESLint version, and additionally guard the `config-array` API so ESLint 8.21-8.27 (`isIgnored`, no `isFileIgnored`/`isDirectoryIgnored`) works, rather than crashing with `configArray.normalizeSync is not a function` (ESLint 7, `config-array` 0.5.0) or a spurious "neither could be loaded" throw (ESLint 2-6).
1 parent f46afff commit bf2d2aa

2 files changed

Lines changed: 68 additions & 15 deletions

File tree

src/core/flatConfigIgnores.js

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'fs';
22
import { dirname, join, resolve } from 'path';
3+
import semver from 'semver';
34

45
// the sync-loadable subset of ESLint's flat config filenames, in its lookup priority order.
56
// `.ts`/`.mts`/`.cts` are intentionally omitted: they require a loader and cannot be `require`d.
@@ -74,20 +75,47 @@ function resolveConfigArray(name) {
7475
}
7576
}
7677

78+
// Flat config only exists as of ESLint 8.21 (`FlatESLint` and the `ESLINT_USE_FLAT_CONFIG` opt-in):
79+
// older versions never read `eslint.config.js`, so a discovered one is inert there and filtering by
80+
// its `ignores` would drop files that ESLint itself would lint.
81+
function supportsFlatConfig() {
82+
try {
83+
// eslint-disable-next-line global-require
84+
return semver.gte(require('eslint/package.json').version, '8.21.0');
85+
} catch (eslintResolveError) {
86+
// `eslint` itself is not resolvable: assume a modern, flat-config-capable install.
87+
return true;
88+
}
89+
}
90+
91+
// The minimum usable config-array API: `normalizeSync` plus a per-file ignore check - `isFileIgnored`
92+
// since v0.11 (and in every `@eslint/config-array`), or its earlier spelling `isIgnored` in v0.9–v0.10
93+
// (shipped with ESLint 8.21–8.27). ESLint 7.32 ships v0.5, which predates all of these.
94+
function isUsableConfigArray(ConfigArray) {
95+
return typeof ConfigArray === 'function'
96+
&& typeof ConfigArray.prototype.normalizeSync === 'function'
97+
&& (typeof ConfigArray.prototype.isFileIgnored === 'function' || typeof ConfigArray.prototype.isIgnored === 'function');
98+
}
99+
100+
function tryResolveConfigArray(name) {
101+
try {
102+
const ConfigArray = resolveConfigArray(name);
103+
return isUsableConfigArray(ConfigArray) ? ConfigArray : null;
104+
} catch (resolveError) {
105+
return null;
106+
}
107+
}
108+
77109
// ESLint 9.4+/10 ship the flat config engine as `@eslint/config-array`; ESLint 8 and 9.0–9.3 ship the
78110
// same API under its pre-fork name `@humanwhocodes/config-array`. Resolved lazily (only on this
79111
// flat-config fallback path). If we are honoring flat config but neither resolves, that is a broken
80-
// install throw rather than silently scan ignored files.
112+
// install - throw rather than silently scan ignored files.
81113
function getConfigArray() {
82-
try {
83-
return resolveConfigArray('@eslint/config-array');
84-
} catch (eslintConfigArrayError) {
85-
try {
86-
return resolveConfigArray('@humanwhocodes/config-array');
87-
} catch (humanwhocodesConfigArrayError) {
88-
throw new Error('eslint-plugin-import: honoring flat config `ignores` in `no-unused-modules` requires `@eslint/config-array` (ESLint 9.4+) or `@humanwhocodes/config-array` (ESLint 8–9.3); neither could be loaded.');
89-
}
114+
const ConfigArray = tryResolveConfigArray('@eslint/config-array') || tryResolveConfigArray('@humanwhocodes/config-array');
115+
if (!ConfigArray) {
116+
throw new Error('eslint-plugin-import: honoring flat config `ignores` in `no-unused-modules` requires `@eslint/config-array` (ESLint 9.4+) or `@humanwhocodes/config-array` (ESLint 8–9.3); neither could be loaded.');
90117
}
118+
return ConfigArray;
91119
}
92120

93121
/**
@@ -96,9 +124,14 @@ function getConfigArray() {
96124
* @param {string} cwd - directory to resolve the flat config from
97125
* @returns {{ isFileIgnored: (p: string) => boolean, isDirectoryIgnored: (p: string) => boolean } | null}
98126
* a predicate over absolute paths, or `null` when no flat config / global ignores apply.
99-
* @throws if a flat config with global `ignores` is present but no config-array implementation resolves.
127+
* @throws if, on a flat-config-capable ESLint, a flat config with global `ignores` is present but no
128+
* usable config-array implementation resolves.
100129
*/
101130
function buildPredicate(cwd) {
131+
if (!supportsFlatConfig()) {
132+
return null;
133+
}
134+
102135
const configPath = findConfigFile(cwd);
103136
if (!configPath) {
104137
return null;
@@ -126,8 +159,14 @@ function buildPredicate(cwd) {
126159
const configArray = new ConfigArray(configs, { basePath: dirname(configPath), schema: {} });
127160
configArray.normalizeSync();
128161
return {
129-
isFileIgnored: (absolutePath) => configArray.isFileIgnored(absolutePath),
130-
isDirectoryIgnored: (absolutePath) => configArray.isDirectoryIgnored(absolutePath),
162+
isFileIgnored: typeof configArray.isFileIgnored === 'function'
163+
? (absolutePath) => configArray.isFileIgnored(absolutePath)
164+
: (absolutePath) => configArray.isIgnored(absolutePath),
165+
// config-array < v0.11 has no `isDirectoryIgnored`; it only prunes the walk, so files under an
166+
// ignored directory are still filtered one-by-one by `isFileIgnored` above.
167+
isDirectoryIgnored: typeof configArray.isDirectoryIgnored === 'function'
168+
? (absolutePath) => configArray.isDirectoryIgnored(absolutePath)
169+
: () => false,
131170
};
132171
}
133172

tests/src/core/listFilesWithNodeFs.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'path';
33
import mockProperty from 'mock-property';
44

55
import listFilesWithNodeFs from 'core/listFilesWithNodeFs';
6-
import { getFilename } from '../utils';
6+
import { eslintVersionSatisfies, getFilename } from '../utils';
77

88
// Fixture layout under tests/files/listFilesWithNodeFs/:
99
// README.md top.js top.ts
@@ -105,9 +105,23 @@ describe('listFilesWithNodeFs, flat-config ignores', function () {
105105
const igRoot = getFilename('listFilesWithNodeFs-flatignores');
106106
const g = (...segments) => path.join(igRoot, ...segments);
107107

108-
it('honors the flat config global `ignores` resolved from cwd', function () {
108+
// flat config only exists as of ESLint 8.21: older versions never read `eslint.config.js`,
109+
// so a discovered one is inert there and nothing may be filtered by it
110+
const supportsFlatConfig = eslintVersionSatisfies('>= 8.21');
111+
112+
(supportsFlatConfig ? it : it.skip)('honors the flat config global `ignores` resolved from cwd', function () {
113+
expect(listFilesWithNodeFs([igRoot], ['.js'], igRoot).sort()).to.deep.equal([
114+
g('eslint.config.js'),
115+
g('keep.js'),
116+
g('src', 'a.js'),
117+
]);
118+
});
119+
120+
(supportsFlatConfig ? it.skip : it)('ignores `eslint.config.js` entirely on an ESLint without flat config support', function () {
109121
expect(listFilesWithNodeFs([igRoot], ['.js'], igRoot).sort()).to.deep.equal([
110122
g('eslint.config.js'),
123+
g('ignored-dir', 'nested.js'),
124+
g('ignored-file.js'),
111125
g('keep.js'),
112126
g('src', 'a.js'),
113127
]);
@@ -124,7 +138,7 @@ describe('listFilesWithNodeFs, flat-config ignores', function () {
124138
]);
125139
});
126140

127-
it('throws, rather than silently scanning ignored files, when a flat config has global `ignores` but no config-array implementation resolves', function () {
141+
(supportsFlatConfig ? it : it.skip)('throws, rather than silently scanning ignored files, when a flat config has global `ignores` but no config-array implementation resolves', function () {
128142
// stub every installed config-array copy (resolved both directly and from eslint's dir, as the
129143
// code does) so accessing `ConfigArray` throws; copies that aren't installed already throw.
130144
const eslintDir = path.dirname(require.resolve('eslint/package.json'));

0 commit comments

Comments
 (0)