Skip to content

Commit

Permalink
feat: add flat config support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
michaelfaith committed Aug 18, 2024
1 parent dd81308 commit f02ff85
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 27 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 45 additions & 0 deletions src/core/fsWalk.js
Original file line number Diff line number Diff line change
@@ -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;
};
78 changes: 52 additions & 26 deletions src/rules/no-unused-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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))
);
},
});
Expand Down Expand Up @@ -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}`,
),
),
);

Expand All @@ -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 {
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
),
);
}
Expand Down Expand Up @@ -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 });
Expand All @@ -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)) {
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
Expand All @@ -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,
);
});
}
});
Expand Down

0 comments on commit f02ff85

Please sign in to comment.