diff --git a/src/FileEnumeratorIsh.js b/src/FileEnumeratorIsh.js new file mode 100644 index 0000000000..eb8cca2135 --- /dev/null +++ b/src/FileEnumeratorIsh.js @@ -0,0 +1,225 @@ +const fs = require("fs"); +const path = require("path"); +const escapeRegExp = require("escape-string-regexp"); + +const dotfilesPattern = /(?:(?:^\.)|(?:[/\\]\.))[^/\\.].*/u; +const NONE = 0; +const IGNORED_SILENTLY = 1; +const IGNORED = 2; + +/** + * @typedef {Object} FileEnumeratorOptions + * @property {CascadingConfigArrayFactory} [configArrayFactory] The factory for config arrays. + * @property {string} [cwd] The base directory to start lookup. + * @property {string[]} [extensions] The extensions to match files for directory patterns. + * @property {(directoryPath: string) => boolean} [isDirectoryIgnored] Returns whether a directory is ignored. + * @property {(filePath: string) => boolean} [isFileIgnored] Returns whether a file is ignored. + */ + +/** + * @typedef {Object} FileAndIgnored + * @property {string} filePath The path to a target file. + * @property {boolean} ignored If `true` then this file should be ignored and warned because it was directly specified. + */ + +/** + * @typedef {Object} FileEntry + * @property {string} filePath The path to a target file. + * @property {ConfigArray} config The config entries of that file. + * @property {NONE|IGNORED_SILENTLY|IGNORED} flag The flag. + * - `NONE` means the file is a target file. + * - `IGNORED_SILENTLY` means the file should be ignored silently. + * - `IGNORED` means the file should be ignored and warned because it was directly specified. + */ + +/** + * Get stats of a given path. + * @param {string} filePath The path to target file. + * @throws {Error} As may be thrown by `fs.statSync`. + * @returns {fs.Stats|null} The stats. + * @private + */ +function statSafeSync(filePath) { + try { + return fs.statSync(filePath); + } catch (error) { + /* c8 ignore next */ + if (error.code !== "ENOENT") { + throw error; + } + return null; + } +} + +/** + * Get filenames in a given path to a directory. + * @param {string} directoryPath The path to target directory. + * @throws {Error} As may be thrown by `fs.readdirSync`. + * @returns {import("fs").Dirent[]} The filenames. + * @private + */ +function readdirSafeSync(directoryPath) { + try { + return fs.readdirSync(directoryPath, { withFileTypes: true }); + } catch (error) { + /* c8 ignore next */ + if (error.code !== "ENOENT") { + throw error; + } + return []; + } +} + +/** + * Create a `RegExp` object to detect extensions. + * @param {string[] | null} extensions The extensions to create. + * @returns {RegExp | null} The created `RegExp` object or null. + */ +function createExtensionRegExp(extensions) { + if (extensions) { + const normalizedExts = extensions.map((ext) => + escapeRegExp(ext.startsWith(".") ? ext.slice(1) : ext) + ); + + return new RegExp(`.\\.(?:${normalizedExts.join("|")})$`, "u"); + } + return null; +} + +/** + * This class provides the functionality that enumerates every file which is + * matched by given glob patterns and that configuration. + */ +export class FileEnumeratorIsh { + /** + * Initialize this enumerator. + * @param {FileEnumeratorOptions} options The options. + */ + constructor({ + cwd = process.cwd(), + extensions = null, + isDirectoryIgnored, + isFileIgnored, + } = {}) { + this.cwd = cwd; + this.extensionRegExp = createExtensionRegExp(extensions); + this.isDirectoryIgnored = isDirectoryIgnored; + this.isFileIgnored = isFileIgnored; + } + + /** + * Iterate files which are matched by given glob patterns. + * @param {string|string[]} patternOrPatterns The glob patterns to iterate files. + * @returns {IterableIterator} The found files. + */ + *iterateFiles(patternOrPatterns) { + const patterns = Array.isArray(patternOrPatterns) + ? patternOrPatterns + : [patternOrPatterns]; + + // The set of paths to remove duplicate. + const set = new Set(); + + for (const pattern of patterns) { + // Skip empty string. + if (!pattern) { + continue; + } + + // Iterate files of this pattern. + for (const { filePath, flag } of this._iterateFiles(pattern)) { + foundRegardlessOfIgnored = true; + if (flag === IGNORED_SILENTLY) { + continue; + } + found = true; + + // Remove duplicate paths while yielding paths. + if (!set.has(filePath)) { + set.add(filePath); + yield { + filePath, + ignored: flag === IGNORED, + }; + } + } + } + } + + /** + * Iterate files which are matched by a given glob pattern. + * @param {string} pattern The glob pattern to iterate files. + * @returns {IterableIterator} The found files. + */ + _iterateFiles(pattern) { + const { cwd } = this; + const absolutePath = path.resolve(cwd, pattern); + const isDot = dotfilesPattern.test(pattern); + const stat = statSafeSync(absolutePath); + + if (!stat) { + return []; + } + + if (stat.isDirectory()) { + return this._iterateFilesWithDirectory(absolutePath, isDot); + } + + if (stat.isFile()) { + return this._iterateFilesWithFile(absolutePath); + } + } + + /** + * Iterate files in a given path. + * @param {string} directoryPath The path to the target directory. + * @param {boolean} dotfiles If `true` then it doesn't skip dot files by default. + * @returns {IterableIterator} The found files. + * @private + */ + _iterateFilesWithDirectory(directoryPath, dotfiles) { + return this._iterateFilesRecursive(directoryPath, { dotfiles }); + } + + /** + * Iterate files in a given path. + * @param {string} directoryPath The path to the target directory. + * @param {Object} options The options to iterate files. + * @param {boolean} [options.dotfiles] If `true` then it doesn't skip dot files by default. + * @param {boolean} [options.recursive] If `true` then it dives into sub directories. + * @returns {IterableIterator} The found files. + * @private + */ + *_iterateFilesRecursive(directoryPath, options) { + // Enumerate the files of this directory. + for (const entry of readdirSafeSync(directoryPath)) { + const filePath = path.join(directoryPath, entry.name); + const fileInfo = entry.isSymbolicLink() ? statSafeSync(filePath) : entry; + + if (!fileInfo) { + continue; + } + + // Check if the file is matched. + if (fileInfo.isFile()) { + if (this.extensionRegExp.test(filePath)) { + const ignored = this.isFileIgnored(filePath, options.dotfiles); + const flag = ignored ? IGNORED_SILENTLY : NONE; + + yield { filePath, flag }; + } + + // Dive into the sub directory. + } else if (fileInfo.isDirectory()) { + const ignored = this.isDirectoryIgnored( + filePath + path.sep, + options.dotfiles + ); + + if (!ignored) { + yield* this._iterateFilesRecursive(filePath, options); + } + } + } + } +} diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js index ec3425dacd..d864f1427a 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -15,53 +15,22 @@ import flatMap from 'array.prototype.flatmap'; import Exports, { recursivePatternCapture } from '../ExportMap'; import docsUrl from '../docsUrl'; +import { FileEnumeratorIsh } from '../FileEnumeratorIsh'; -let FileEnumerator; -let listFilesToProcess; - -try { - ({ FileEnumerator } = require('eslint/use-at-your-own-risk')); -} catch (e) { - try { - // has been moved to eslint/lib/cli-engine/file-enumerator in version 6 - ({ FileEnumerator } = require('eslint/lib/cli-engine/file-enumerator')); - } catch (e) { - try { - // eslint/lib/util/glob-util has been moved to eslint/lib/util/glob-utils with version 5.3 - const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-utils'); - - // Prevent passing invalid options (extensions array) to old versions of the function. - // https://github.com/eslint/eslint/blob/v5.16.0/lib/util/glob-utils.js#L178-L280 - // https://github.com/eslint/eslint/blob/v5.2.0/lib/util/glob-util.js#L174-L269 - listFilesToProcess = function (src, extensions) { - return originalListFilesToProcess(src, { - extensions, - }); - }; - } catch (e) { - const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-util'); - - listFilesToProcess = function (src, extensions) { - const patterns = src.concat(flatMap(src, (pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`))); - - return originalListFilesToProcess(patterns); - }; - } - } -} +const listFilesToProcess = function (context, src) { + const extensions = Array.from(getFileExtensions(context.settings)); -if (FileEnumerator) { - listFilesToProcess = function (src, extensions) { - const e = new FileEnumerator({ - extensions, - }); + const e = new FileEnumeratorIsh({ + cwd: context.cwd, + extensions, + ...context.session, + }); - return Array.from(e.iterateFiles(src), ({ filePath, ignored }) => ({ - ignored, - filename: filePath, - })); - }; -} + return Array.from(e.iterateFiles(src), ({ filePath, ignored }) => ({ + ignored, + filename: filePath, + })); +}; const EXPORT_DEFAULT_DECLARATION = 'ExportDefaultDeclaration'; const EXPORT_NAMED_DECLARATION = 'ExportNamedDeclaration'; @@ -171,12 +140,10 @@ const isNodeModule = (path) => (/\/(node_modules)\//).test(path); * return all files matching src pattern, which are not matching the ignoreExports pattern */ const resolveFiles = (src, ignoreExports, context) => { - const extensions = Array.from(getFileExtensions(context.settings)); - - const srcFileList = listFilesToProcess(src, extensions); + const srcFileList = listFilesToProcess(context, src); // prepare list of ignored files - const ignoredFilesList = listFilesToProcess(ignoreExports, extensions); + const ignoredFilesList = listFilesToProcess(context, ignoreExports); ignoredFilesList.forEach(({ filename }) => ignoredFiles.add(filename)); // prepare list of source files, don't consider files from node_modules