Skip to content

Commit dce4b99

Browse files
committed
feat: add flat config support
This change adds support for ESLint's new Flat config system. It maintains backwards compatibility with eslintrc style configs as well. To achieve this, we're now dynamically creating flat configs on a new `flatConfigs` export. I was a bit on the fence about using this convention, or the other convention that's become prevalent in the community: adding the flat configs directly to the `configs` object, but with a 'flat/' prefix. I like this better, since it's slightly more ergonomic when using it in practice. e.g. `...importX.flatConfigs.recommended` vs `...importX.configs['flat/recommended']`, but i'm open to changing that. Example Usage ```js import importPlugin from 'eslint-plugin-import'; import js from '@eslint/js'; import tsParser from '@typescript-eslint/parser'; export default [ js.configs.recommended, importPlugin.flatConfigs.recommended, importPlugin.flatConfigs.react, importPlugin.flatConfigs.typescript, { files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], languageOptions: { parser: tsParser, ecmaVersion: 'latest', sourceType: 'module', }, ignores: ['eslint.config.js'], rules: { 'no-unused-vars': 'off', 'import/no-dynamic-require': 'warn', 'import/no-nodejs-modules': 'warn', }, }, ]; ``` Note: in order to fill a gap in a future API gap for the `no-unused-module`, this takes advantage of a *proposed* new API on the ESLint context, that currently only exists in a POC state (unreleased). Closes import-js#2556
1 parent e2cb799 commit dce4b99

File tree

3 files changed

+106
-33
lines changed

3 files changed

+106
-33
lines changed

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@
108108
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
109109
},
110110
"dependencies": {
111-
"@nodelib/fs.walk": "^2.0.0",
112111
"array-includes": "^3.1.7",
113112
"array.prototype.findlastindex": "^1.2.4",
114113
"array.prototype.flat": "^1.3.2",

src/core/fsWalk.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* This is intended to provide similar capability as the sync api from @nodelib/fs.walk, until `eslint-plugin-import`
3+
* is willing to modernize and update their minimum node version to at least v16. I intentionally made the
4+
* shape of the API (for the part we're using) the same as @nodelib/fs.walk so that that can be swapped in
5+
* when the repo is ready for it.
6+
*/
7+
8+
import path from 'path';
9+
10+
/**
11+
* Do a comprehensive walk of the provided src directory, and collect all entries. Filter out
12+
* any directories or entries using the optional filter functions.
13+
* @param {string} root - path to the root of the folder we're walking
14+
* @param {{ deepFilter?: ({name: string, path: string, dirent: Dirent}) => boolean, entryFilter?: ({name: string, path: string, dirent: Dirent}) => boolean }} options
15+
* @param {{name: string, path: string, dirent: Dirent}} currentEntry - entry for the current directory we're working in
16+
* @param {{name: string, path: string, dirent: Dirent}[]} existingEntries - list of all entries so far
17+
* @returns {{name: string, path: string, dirent: Dirent}[]} an array of directory entries
18+
*/
19+
export const walkSync = (root, options, currentEntry, existingEntries) => {
20+
const { readdirSync } = require('node:fs');
21+
22+
// Extract the filter functions. Default to evaluating true, if no filter passed in.
23+
const { deepFilter = (_) => true, entryFilter = (_) => true } = options;
24+
25+
let entryList = existingEntries || [];
26+
const currentRelativePath = currentEntry ? currentEntry.path : '.';
27+
const fullPath = currentEntry ? path.join(root, currentEntry.path) : root;
28+
29+
const dirents = readdirSync(fullPath, { withFileTypes: true });
30+
for (const dirent of dirents) {
31+
const entry = {
32+
name: dirent.name,
33+
path: path.join(currentRelativePath, dirent.name),
34+
dirent,
35+
};
36+
if (dirent.isDirectory() && deepFilter(entry)) {
37+
entryList.push(entry);
38+
entryList = walkSync(root, options, entry, entryList);
39+
} else if (dirent.isFile() && entryFilter(entry)) {
40+
entryList.push(entry);
41+
}
42+
}
43+
44+
return entryList;
45+
};

src/rules/no-unused-modules.js

+61-32
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* @author René Fermann
55
*/
66

7-
import * as fsWalk from '@nodelib/fs.walk';
7+
import * as fsWalk from '../core/fsWalk';
88
import { getFileExtensions } from 'eslint-module-utils/ignore';
99
import resolve from 'eslint-module-utils/resolve';
1010
import visit from 'eslint-module-utils/visit';
@@ -44,8 +44,8 @@ function listFilesWithModernApi(srcPaths, extensions, session) {
4444

4545
// Include the file if it's not marked as ignore by eslint and its extension is included in our list
4646
return (
47-
!session.isFileIgnored(fullEntryPath)
48-
&& extensions.find((extension) => entry.path.endsWith(extension))
47+
!session.isFileIgnored(fullEntryPath) &&
48+
extensions.find((extension) => entry.path.endsWith(extension))
4949
);
5050
},
5151
});
@@ -140,8 +140,10 @@ function listFilesWithLegacyFunctions(src, extensions) {
140140
listFilesToProcess: originalListFilesToProcess,
141141
} = require('eslint/lib/util/glob-util');
142142
const patterns = src.concat(
143-
flatMap(src, (pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`,
144-
),
143+
flatMap(src, (pattern) =>
144+
extensions.map((extension) =>
145+
/\*\*|\*\./.test(pattern) ? pattern : `${pattern}/**/*${extension}`,
146+
),
145147
),
146148
);
147149

@@ -162,9 +164,9 @@ function listFilesToProcess(src, extensions, context) {
162164
// Otherwise, fallback to using the deprecated `FileEnumerator` for legacy support.
163165
// https://github.com/eslint/eslint/issues/18087
164166
if (
165-
context.session
166-
&& context.session.isFileIgnored
167-
&& context.session.isDirectoryIgnored
167+
context.session &&
168+
context.session.isFileIgnored &&
169+
context.session.isDirectoryIgnored
168170
) {
169171
return listFilesWithModernApi(src, extensions, context.session);
170172
} else {
@@ -175,7 +177,7 @@ function listFilesToProcess(src, extensions, context) {
175177
if (FileEnumerator) {
176178
return listFilesUsingFileEnumerator(FileEnumerator, src, extensions);
177179
} else {
178-
// If not, then we can try even older versions of this capability (listFilesToProcess)
180+
// If not, then we can try even older versions of this capability (listFilesToProcess)
179181
return listFilesWithLegacyFunctions(src, extensions);
180182
}
181183
}
@@ -201,11 +203,11 @@ const DEFAULT = 'default';
201203
function forEachDeclarationIdentifier(declaration, cb) {
202204
if (declaration) {
203205
if (
204-
declaration.type === FUNCTION_DECLARATION
205-
|| declaration.type === CLASS_DECLARATION
206-
|| declaration.type === TS_INTERFACE_DECLARATION
207-
|| declaration.type === TS_TYPE_ALIAS_DECLARATION
208-
|| declaration.type === TS_ENUM_DECLARATION
206+
declaration.type === FUNCTION_DECLARATION ||
207+
declaration.type === CLASS_DECLARATION ||
208+
declaration.type === TS_INTERFACE_DECLARATION ||
209+
declaration.type === TS_TYPE_ALIAS_DECLARATION ||
210+
declaration.type === TS_ENUM_DECLARATION
209211
) {
210212
cb(declaration.id.name);
211213
} else if (declaration.type === VARIABLE_DECLARATION) {
@@ -281,7 +283,7 @@ const visitorKeyMap = new Map();
281283
const ignoredFiles = new Set();
282284
const filesOutsideSrc = new Set();
283285

284-
const isNodeModule = (path) => (/\/(node_modules)\//).test(path);
286+
const isNodeModule = (path) => /\/(node_modules)\//.test(path);
285287

286288
/**
287289
* read all files matching the patterns in src and ignoreExports
@@ -315,7 +317,8 @@ const resolveFiles = (src, ignoreExports, context) => {
315317
);
316318
} else {
317319
resolvedFiles = new Set(
318-
flatMap(srcFileList, ({ filename }) => isNodeModule(filename) ? [] : filename,
320+
flatMap(srcFileList, ({ filename }) =>
321+
isNodeModule(filename) ? [] : filename,
319322
),
320323
);
321324
}
@@ -485,9 +488,11 @@ const doPreparation = (src, ignoreExports, context) => {
485488
lastPrepareKey = prepareKey;
486489
};
487490

488-
const newNamespaceImportExists = (specifiers) => specifiers.some(({ type }) => type === IMPORT_NAMESPACE_SPECIFIER);
491+
const newNamespaceImportExists = (specifiers) =>
492+
specifiers.some(({ type }) => type === IMPORT_NAMESPACE_SPECIFIER);
489493

490-
const newDefaultImportExists = (specifiers) => specifiers.some(({ type }) => type === IMPORT_DEFAULT_SPECIFIER);
494+
const newDefaultImportExists = (specifiers) =>
495+
specifiers.some(({ type }) => type === IMPORT_DEFAULT_SPECIFIER);
491496

492497
const fileIsInPkg = (file) => {
493498
const { path, pkg } = readPkgUp({ cwd: file });
@@ -500,7 +505,8 @@ const fileIsInPkg = (file) => {
500505
};
501506

502507
const checkPkgFieldObject = (pkgField) => {
503-
const pkgFieldFiles = flatMap(values(pkgField), (value) => typeof value === 'boolean' ? [] : join(basePath, value),
508+
const pkgFieldFiles = flatMap(values(pkgField), (value) =>
509+
typeof value === 'boolean' ? [] : join(basePath, value),
504510
);
505511

506512
if (includes(pkgFieldFiles, file)) {
@@ -673,14 +679,16 @@ module.exports = {
673679
exports = exportList.get(file);
674680

675681
if (!exports) {
676-
console.error(`file \`${file}\` has no exports. Please update to the latest, and if it still happens, report this on https://github.com/import-js/eslint-plugin-import/issues/2866!`);
682+
console.error(
683+
`file \`${file}\` has no exports. Please update to the latest, and if it still happens, report this on https://github.com/import-js/eslint-plugin-import/issues/2866!`,
684+
);
677685
}
678686

679687
// special case: export * from
680688
const exportAll = exports.get(EXPORT_ALL_DECLARATION);
681689
if (
682-
typeof exportAll !== 'undefined'
683-
&& exportedValue !== IMPORT_DEFAULT_SPECIFIER
690+
typeof exportAll !== 'undefined' &&
691+
exportedValue !== IMPORT_DEFAULT_SPECIFIER
684692
) {
685693
if (exportAll.whereUsed.size > 0) {
686694
return;
@@ -696,11 +704,13 @@ module.exports = {
696704
}
697705

698706
// exportsList will always map any imported value of 'default' to 'ImportDefaultSpecifier'
699-
const exportsKey = exportedValue === DEFAULT ? IMPORT_DEFAULT_SPECIFIER : exportedValue;
707+
const exportsKey =
708+
exportedValue === DEFAULT ? IMPORT_DEFAULT_SPECIFIER : exportedValue;
700709

701710
const exportStatement = exports.get(exportsKey);
702711

703-
const value = exportsKey === IMPORT_DEFAULT_SPECIFIER ? DEFAULT : exportsKey;
712+
const value =
713+
exportsKey === IMPORT_DEFAULT_SPECIFIER ? DEFAULT : exportsKey;
704714

705715
if (typeof exportStatement !== 'undefined') {
706716
if (exportStatement.whereUsed.size < 1) {
@@ -823,8 +833,8 @@ module.exports = {
823833
}
824834
value.forEach((val) => {
825835
if (
826-
val !== IMPORT_NAMESPACE_SPECIFIER
827-
&& val !== IMPORT_DEFAULT_SPECIFIER
836+
val !== IMPORT_NAMESPACE_SPECIFIER &&
837+
val !== IMPORT_DEFAULT_SPECIFIER
828838
) {
829839
oldImports.set(val, key);
830840
}
@@ -859,7 +869,10 @@ module.exports = {
859869
// support for export { value } from 'module'
860870
if (astNode.type === EXPORT_NAMED_DECLARATION) {
861871
if (astNode.source) {
862-
resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context);
872+
resolvedPath = resolve(
873+
astNode.source.raw.replace(/('|")/g, ''),
874+
context,
875+
);
863876
astNode.specifiers.forEach((specifier) => {
864877
const name = specifier.local.name || specifier.local.value;
865878
if (name === DEFAULT) {
@@ -872,12 +885,18 @@ module.exports = {
872885
}
873886

874887
if (astNode.type === EXPORT_ALL_DECLARATION) {
875-
resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context);
888+
resolvedPath = resolve(
889+
astNode.source.raw.replace(/('|")/g, ''),
890+
context,
891+
);
876892
newExportAll.add(resolvedPath);
877893
}
878894

879895
if (astNode.type === IMPORT_DECLARATION) {
880-
resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context);
896+
resolvedPath = resolve(
897+
astNode.source.raw.replace(/('|")/g, ''),
898+
context,
899+
);
881900
if (!resolvedPath) {
882901
return;
883902
}
@@ -895,9 +914,16 @@ module.exports = {
895914
}
896915

897916
astNode.specifiers
898-
.filter((specifier) => specifier.type !== IMPORT_DEFAULT_SPECIFIER && specifier.type !== IMPORT_NAMESPACE_SPECIFIER)
917+
.filter(
918+
(specifier) =>
919+
specifier.type !== IMPORT_DEFAULT_SPECIFIER &&
920+
specifier.type !== IMPORT_NAMESPACE_SPECIFIER,
921+
)
899922
.forEach((specifier) => {
900-
newImports.set(specifier.imported.name || specifier.imported.value, resolvedPath);
923+
newImports.set(
924+
specifier.imported.name || specifier.imported.value,
925+
resolvedPath,
926+
);
901927
});
902928
}
903929
});
@@ -1086,7 +1112,10 @@ module.exports = {
10861112
},
10871113
ExportNamedDeclaration(node) {
10881114
node.specifiers.forEach((specifier) => {
1089-
checkUsage(specifier, specifier.exported.name || specifier.exported.value);
1115+
checkUsage(
1116+
specifier,
1117+
specifier.exported.name || specifier.exported.value,
1118+
);
10901119
});
10911120
forEachDeclarationIdentifier(node.declaration, (name) => {
10921121
checkUsage(node, name);

0 commit comments

Comments
 (0)