From f02ff8565292f6765d5c80a7e9fa938edb86e395 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 18 Aug 2024 15:56:16 -0500 Subject: [PATCH] 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 #2556 --- package.json | 1 - src/core/fsWalk.js | 45 ++++++++++++++++++++ src/rules/no-unused-modules.js | 78 ++++++++++++++++++++++------------ 3 files changed, 97 insertions(+), 27 deletions(-) create mode 100644 src/core/fsWalk.js diff --git a/package.json b/package.json index aa2b98475f..b6a089bdbb 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,6 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" }, "dependencies": { - "@nodelib/fs.walk": "^2.0.0", "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.4", "array.prototype.flat": "^1.3.2", diff --git a/src/core/fsWalk.js b/src/core/fsWalk.js new file mode 100644 index 0000000000..0e7f979228 --- /dev/null +++ b/src/core/fsWalk.js @@ -0,0 +1,45 @@ +/** + * This is intended to provide similar capability as the sync api from @nodelib/fs.walk, until `eslint-plugin-import` + * is willing to modernize and update their minimum node version to at least v16. I intentionally made the + * shape of the API (for the part we're using) the same as @nodelib/fs.walk so that that can be swapped in + * when the repo is ready for it. + */ + +import path from 'path'; + +/** + * Do a comprehensive walk of the provided src directory, and collect all entries. Filter out + * any directories or entries using the optional filter functions. + * @param {string} root - path to the root of the folder we're walking + * @param {{ deepFilter?: ({name: string, path: string, dirent: Dirent}) => boolean, entryFilter?: ({name: string, path: string, dirent: Dirent}) => boolean }} options + * @param {{name: string, path: string, dirent: Dirent}} currentEntry - entry for the current directory we're working in + * @param {{name: string, path: string, dirent: Dirent}[]} existingEntries - list of all entries so far + * @returns {{name: string, path: string, dirent: Dirent}[]} an array of directory entries + */ +export const walkSync = (root, options, currentEntry, existingEntries) => { + const { readdirSync } = require('node:fs'); + + // Extract the filter functions. Default to evaluating true, if no filter passed in. + const { deepFilter = (_) => true, entryFilter = (_) => true } = options; + + let entryList = existingEntries || []; + const currentRelativePath = currentEntry ? currentEntry.path : '.'; + const fullPath = currentEntry ? path.join(root, currentEntry.path) : root; + + const dirents = readdirSync(fullPath, { withFileTypes: true }); + for (const dirent of dirents) { + const entry = { + name: dirent.name, + path: path.join(currentRelativePath, dirent.name), + dirent, + }; + if (dirent.isDirectory() && deepFilter(entry)) { + entryList.push(entry); + entryList = walkSync(root, options, entry, entryList); + } else if (dirent.isFile() && entryFilter(entry)) { + entryList.push(entry); + } + } + + return entryList; +}; diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js index e7a0ecc090..e045db5587 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -4,7 +4,7 @@ * @author René Fermann */ -import * as fsWalk from '@nodelib/fs.walk'; +import * as fsWalk from '../core/fsWalk'; import { getFileExtensions } from 'eslint-module-utils/ignore'; import resolve from 'eslint-module-utils/resolve'; import visit from 'eslint-module-utils/visit'; @@ -44,8 +44,8 @@ function listFilesWithModernApi(srcPaths, extensions, session) { // Include the file if it's not marked as ignore by eslint and its extension is included in our list return ( - !session.isFileIgnored(fullEntryPath) - && extensions.find((extension) => entry.path.endsWith(extension)) + !session.isFileIgnored(fullEntryPath) && + extensions.find((extension) => entry.path.endsWith(extension)) ); }, }); @@ -140,8 +140,10 @@ function listFilesWithLegacyFunctions(src, extensions) { listFilesToProcess: originalListFilesToProcess, } = require('eslint/lib/util/glob-util'); const patterns = src.concat( - flatMap(src, (pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`, - ), + flatMap(src, (pattern) => + extensions.map((extension) => + /\*\*|\*\./.test(pattern) ? pattern : `${pattern}/**/*${extension}`, + ), ), ); @@ -162,9 +164,9 @@ function listFilesToProcess(src, extensions, context) { // Otherwise, fallback to using the deprecated `FileEnumerator` for legacy support. // https://github.com/eslint/eslint/issues/18087 if ( - context.session - && context.session.isFileIgnored - && context.session.isDirectoryIgnored + context.session && + context.session.isFileIgnored && + context.session.isDirectoryIgnored ) { return listFilesWithModernApi(src, extensions, context.session); } else { @@ -175,7 +177,7 @@ function listFilesToProcess(src, extensions, context) { if (FileEnumerator) { return listFilesUsingFileEnumerator(FileEnumerator, src, extensions); } else { - // If not, then we can try even older versions of this capability (listFilesToProcess) + // If not, then we can try even older versions of this capability (listFilesToProcess) return listFilesWithLegacyFunctions(src, extensions); } } @@ -283,7 +285,7 @@ const visitorKeyMap = new Map(); const ignoredFiles = new Set(); const filesOutsideSrc = new Set(); -const isNodeModule = (path) => (/\/(node_modules)\//).test(path); +const isNodeModule = (path) => /\/(node_modules)\//.test(path); /** * read all files matching the patterns in src and ignoreExports @@ -317,7 +319,8 @@ const resolveFiles = (src, ignoreExports, context) => { ); } else { resolvedFiles = new Set( - flatMap(srcFileList, ({ filename }) => isNodeModule(filename) ? [] : filename, + flatMap(srcFileList, ({ filename }) => + isNodeModule(filename) ? [] : filename, ), ); } @@ -487,9 +490,11 @@ const doPreparation = (src, ignoreExports, context) => { lastPrepareKey = prepareKey; }; -const newNamespaceImportExists = (specifiers) => specifiers.some(({ type }) => type === IMPORT_NAMESPACE_SPECIFIER); +const newNamespaceImportExists = (specifiers) => + specifiers.some(({ type }) => type === IMPORT_NAMESPACE_SPECIFIER); -const newDefaultImportExists = (specifiers) => specifiers.some(({ type }) => type === IMPORT_DEFAULT_SPECIFIER); +const newDefaultImportExists = (specifiers) => + specifiers.some(({ type }) => type === IMPORT_DEFAULT_SPECIFIER); const fileIsInPkg = (file) => { const { path, pkg } = readPkgUp({ cwd: file }); @@ -502,7 +507,8 @@ const fileIsInPkg = (file) => { }; const checkPkgFieldObject = (pkgField) => { - const pkgFieldFiles = flatMap(values(pkgField), (value) => typeof value === 'boolean' ? [] : join(basePath, value), + const pkgFieldFiles = flatMap(values(pkgField), (value) => + typeof value === 'boolean' ? [] : join(basePath, value), ); if (includes(pkgFieldFiles, file)) { @@ -681,14 +687,16 @@ module.exports = { exports = exportList.get(file); if (!exports) { - 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!`); + 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!`, + ); } // special case: export * from const exportAll = exports.get(EXPORT_ALL_DECLARATION); if ( - typeof exportAll !== 'undefined' - && exportedValue !== IMPORT_DEFAULT_SPECIFIER + typeof exportAll !== 'undefined' && + exportedValue !== IMPORT_DEFAULT_SPECIFIER ) { if (exportAll.whereUsed.size > 0) { return; @@ -704,11 +712,13 @@ module.exports = { } // exportsList will always map any imported value of 'default' to 'ImportDefaultSpecifier' - const exportsKey = exportedValue === DEFAULT ? IMPORT_DEFAULT_SPECIFIER : exportedValue; + const exportsKey = + exportedValue === DEFAULT ? IMPORT_DEFAULT_SPECIFIER : exportedValue; const exportStatement = exports.get(exportsKey); - const value = exportsKey === IMPORT_DEFAULT_SPECIFIER ? DEFAULT : exportsKey; + const value = + exportsKey === IMPORT_DEFAULT_SPECIFIER ? DEFAULT : exportsKey; if (typeof exportStatement !== 'undefined') { if (exportStatement.whereUsed.size < 1) { @@ -831,8 +841,8 @@ module.exports = { } value.forEach((val) => { if ( - val !== IMPORT_NAMESPACE_SPECIFIER - && val !== IMPORT_DEFAULT_SPECIFIER + val !== IMPORT_NAMESPACE_SPECIFIER && + val !== IMPORT_DEFAULT_SPECIFIER ) { oldImports.set(val, key); } @@ -867,7 +877,10 @@ module.exports = { // support for export { value } from 'module' if (astNode.type === EXPORT_NAMED_DECLARATION) { if (astNode.source) { - resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context); + resolvedPath = resolve( + astNode.source.raw.replace(/('|")/g, ''), + context, + ); astNode.specifiers.forEach((specifier) => { const name = specifier.local.name || specifier.local.value; if (name === DEFAULT) { @@ -880,12 +893,18 @@ module.exports = { } if (astNode.type === EXPORT_ALL_DECLARATION) { - resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context); + resolvedPath = resolve( + astNode.source.raw.replace(/('|")/g, ''), + context, + ); newExportAll.add(resolvedPath); } if (astNode.type === IMPORT_DECLARATION) { - resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context); + resolvedPath = resolve( + astNode.source.raw.replace(/('|")/g, ''), + context, + ); if (!resolvedPath) { return; } @@ -903,9 +922,16 @@ module.exports = { } astNode.specifiers - .filter((specifier) => specifier.type !== IMPORT_DEFAULT_SPECIFIER && specifier.type !== IMPORT_NAMESPACE_SPECIFIER) + .filter( + (specifier) => + specifier.type !== IMPORT_DEFAULT_SPECIFIER && + specifier.type !== IMPORT_NAMESPACE_SPECIFIER, + ) .forEach((specifier) => { - newImports.set(specifier.imported.name || specifier.imported.value, resolvedPath); + newImports.set( + specifier.imported.name || specifier.imported.value, + resolvedPath, + ); }); } });