Spaces:
Sleeping
Sleeping
| /** | |
| * @fileoverview `FileEnumerator` class. | |
| * | |
| * `FileEnumerator` class has two responsibilities: | |
| * | |
| * 1. Find target files by processing glob patterns. | |
| * 2. Tie each target file and appropriate configuration. | |
| * | |
| * It provides a method: | |
| * | |
| * - `iterateFiles(patterns)` | |
| * Iterate files which are matched by given patterns together with the | |
| * corresponded configuration. This is for `CLIEngine#executeOnFiles()`. | |
| * While iterating files, it loads the configuration file of each directory | |
| * before iterate files on the directory, so we can use the configuration | |
| * files to determine target files. | |
| * | |
| * @example | |
| * const enumerator = new FileEnumerator(); | |
| * const linter = new Linter(); | |
| * | |
| * for (const { config, filePath } of enumerator.iterateFiles(["*.js"])) { | |
| * const code = fs.readFileSync(filePath, "utf8"); | |
| * const messages = linter.verify(code, config, filePath); | |
| * | |
| * console.log(messages); | |
| * } | |
| * | |
| * @author Toru Nagashima <https://github.com/mysticatea> | |
| */ | |
| ; | |
| //------------------------------------------------------------------------------ | |
| // Requirements | |
| //------------------------------------------------------------------------------ | |
| const fs = require("node:fs"); | |
| const path = require("node:path"); | |
| const getGlobParent = require("glob-parent"); | |
| const isGlob = require("is-glob"); | |
| const escapeRegExp = require("escape-string-regexp"); | |
| const { Minimatch } = require("minimatch"); | |
| const { | |
| Legacy: { IgnorePattern, CascadingConfigArrayFactory }, | |
| } = require("@eslint/eslintrc"); | |
| const debug = require("debug")("eslint:file-enumerator"); | |
| //------------------------------------------------------------------------------ | |
| // Helpers | |
| //------------------------------------------------------------------------------ | |
| const minimatchOpts = { dot: true, matchBase: true }; | |
| const dotfilesPattern = /(?:^\.|[/\\]\.)[^/\\.].*/u; | |
| const NONE = 0; | |
| const IGNORED_SILENTLY = 1; | |
| const IGNORED = 2; | |
| // For VSCode intellisense | |
| /** @typedef {ReturnType<CascadingConfigArrayFactory.getConfigArrayForFile>} ConfigArray */ | |
| /** | |
| * @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 {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. | |
| * @property {boolean} [ignore] The flag to check ignored files. | |
| * @property {string[]} [rulePaths] The value of `--rulesdir` option. | |
| */ | |
| /** | |
| * @typedef {Object} FileAndConfig | |
| * @property {string} filePath The path to a target file. | |
| * @property {ConfigArray} config The config entries of that 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. | |
| */ | |
| /** | |
| * @typedef {Object} FileEnumeratorInternalSlots | |
| * @property {CascadingConfigArrayFactory} configArrayFactory The factory for config arrays. | |
| * @property {string} cwd The base directory to start lookup. | |
| * @property {RegExp|null} extensionRegExp The RegExp to test if a string ends with specific file extensions. | |
| * @property {boolean} globInputPaths Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. | |
| * @property {boolean} ignoreFlag The flag to check ignored files. | |
| * @property {(filePath:string, dot:boolean) => boolean} defaultIgnores The default predicate function to ignore files. | |
| */ | |
| /** @type {WeakMap<FileEnumerator, FileEnumeratorInternalSlots>} */ | |
| const internalSlotsMap = new WeakMap(); | |
| /** | |
| * Check if a string is a glob pattern or not. | |
| * @param {string} pattern A glob pattern. | |
| * @returns {boolean} `true` if the string is a glob pattern. | |
| */ | |
| function isGlobPattern(pattern) { | |
| return isGlob(path.sep === "\\" ? pattern.replace(/\\/gu, "/") : pattern); | |
| } | |
| /** | |
| * 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; | |
| } | |
| /** | |
| * The error type when no files match a glob. | |
| */ | |
| class NoFilesFoundError extends Error { | |
| /** | |
| * @param {string} pattern The glob pattern which was not found. | |
| * @param {boolean} globDisabled If `true` then the pattern was a glob pattern, but glob was disabled. | |
| */ | |
| constructor(pattern, globDisabled) { | |
| super( | |
| `No files matching '${pattern}' were found${globDisabled ? " (glob was disabled)" : ""}.`, | |
| ); | |
| this.messageTemplate = "file-not-found"; | |
| this.messageData = { pattern, globDisabled }; | |
| } | |
| } | |
| /** | |
| * The error type when there are files matched by a glob, but all of them have been ignored. | |
| */ | |
| class AllFilesIgnoredError extends Error { | |
| /** | |
| * @param {string} pattern The glob pattern which was not found. | |
| */ | |
| constructor(pattern) { | |
| super(`All files matched by '${pattern}' are ignored.`); | |
| this.messageTemplate = "all-files-ignored"; | |
| this.messageData = { pattern }; | |
| } | |
| } | |
| /** | |
| * This class provides the functionality that enumerates every file which is | |
| * matched by given glob patterns and that configuration. | |
| */ | |
| class FileEnumerator { | |
| /** | |
| * Initialize this enumerator. | |
| * @param {FileEnumeratorOptions} options The options. | |
| */ | |
| constructor({ | |
| cwd = process.cwd(), | |
| configArrayFactory = new CascadingConfigArrayFactory({ | |
| cwd, | |
| getEslintRecommendedConfig: () => | |
| require("@eslint/js").configs.recommended, | |
| getEslintAllConfig: () => require("@eslint/js").configs.all, | |
| }), | |
| extensions = null, | |
| globInputPaths = true, | |
| errorOnUnmatchedPattern = true, | |
| ignore = true, | |
| } = {}) { | |
| internalSlotsMap.set(this, { | |
| configArrayFactory, | |
| cwd, | |
| defaultIgnores: IgnorePattern.createDefaultIgnore(cwd), | |
| extensionRegExp: createExtensionRegExp(extensions), | |
| globInputPaths, | |
| errorOnUnmatchedPattern, | |
| ignoreFlag: ignore, | |
| }); | |
| } | |
| /** | |
| * Check if a given file is target or not. | |
| * @param {string} filePath The path to a candidate file. | |
| * @param {ConfigArray} [providedConfig] Optional. The configuration for the file. | |
| * @returns {boolean} `true` if the file is a target. | |
| */ | |
| isTargetPath(filePath, providedConfig) { | |
| const { configArrayFactory, extensionRegExp } = | |
| internalSlotsMap.get(this); | |
| // If `--ext` option is present, use it. | |
| if (extensionRegExp) { | |
| return extensionRegExp.test(filePath); | |
| } | |
| // `.js` file is target by default. | |
| if (filePath.endsWith(".js")) { | |
| return true; | |
| } | |
| // use `overrides[].files` to check additional targets. | |
| const config = | |
| providedConfig || | |
| configArrayFactory.getConfigArrayForFile(filePath, { | |
| ignoreNotFoundError: true, | |
| }); | |
| return config.isAdditionalTargetPath(filePath); | |
| } | |
| /** | |
| * Iterate files which are matched by given glob patterns. | |
| * @param {string|string[]} patternOrPatterns The glob patterns to iterate files. | |
| * @throws {NoFilesFoundError|AllFilesIgnoredError} On an unmatched pattern. | |
| * @returns {IterableIterator<FileAndConfig>} The found files. | |
| */ | |
| *iterateFiles(patternOrPatterns) { | |
| const { globInputPaths, errorOnUnmatchedPattern } = | |
| internalSlotsMap.get(this); | |
| const patterns = Array.isArray(patternOrPatterns) | |
| ? patternOrPatterns | |
| : [patternOrPatterns]; | |
| debug("Start to iterate files: %o", patterns); | |
| // The set of paths to remove duplicate. | |
| const set = new Set(); | |
| for (const pattern of patterns) { | |
| let foundRegardlessOfIgnored = false; | |
| let found = false; | |
| // Skip empty string. | |
| if (!pattern) { | |
| continue; | |
| } | |
| // Iterate files of this pattern. | |
| for (const { config, 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 { | |
| config, | |
| filePath, | |
| ignored: flag === IGNORED, | |
| }; | |
| } | |
| } | |
| // Raise an error if any files were not found. | |
| if (errorOnUnmatchedPattern) { | |
| if (!foundRegardlessOfIgnored) { | |
| throw new NoFilesFoundError( | |
| pattern, | |
| !globInputPaths && isGlob(pattern), | |
| ); | |
| } | |
| if (!found) { | |
| throw new AllFilesIgnoredError(pattern); | |
| } | |
| } | |
| } | |
| debug(`Complete iterating files: ${JSON.stringify(patterns)}`); | |
| } | |
| /** | |
| * Iterate files which are matched by a given glob pattern. | |
| * @param {string} pattern The glob pattern to iterate files. | |
| * @returns {IterableIterator<FileEntry>} The found files. | |
| */ | |
| _iterateFiles(pattern) { | |
| const { cwd, globInputPaths } = internalSlotsMap.get(this); | |
| const absolutePath = path.resolve(cwd, pattern); | |
| const isDot = dotfilesPattern.test(pattern); | |
| const stat = statSafeSync(absolutePath); | |
| if (stat && stat.isDirectory()) { | |
| return this._iterateFilesWithDirectory(absolutePath, isDot); | |
| } | |
| if (stat && stat.isFile()) { | |
| return this._iterateFilesWithFile(absolutePath); | |
| } | |
| if (globInputPaths && isGlobPattern(pattern)) { | |
| return this._iterateFilesWithGlob(pattern, isDot); | |
| } | |
| return []; | |
| } | |
| /** | |
| * Iterate a file which is matched by a given path. | |
| * @param {string} filePath The path to the target file. | |
| * @returns {IterableIterator<FileEntry>} The found files. | |
| * @private | |
| */ | |
| _iterateFilesWithFile(filePath) { | |
| debug(`File: ${filePath}`); | |
| const { configArrayFactory } = internalSlotsMap.get(this); | |
| const config = configArrayFactory.getConfigArrayForFile(filePath); | |
| const ignored = this._isIgnoredFile(filePath, { config, direct: true }); | |
| const flag = ignored ? IGNORED : NONE; | |
| return [{ config, filePath, flag }]; | |
| } | |
| /** | |
| * 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<FileEntry>} The found files. | |
| * @private | |
| */ | |
| _iterateFilesWithDirectory(directoryPath, dotfiles) { | |
| debug(`Directory: ${directoryPath}`); | |
| return this._iterateFilesRecursive(directoryPath, { | |
| dotfiles, | |
| recursive: true, | |
| selector: null, | |
| }); | |
| } | |
| /** | |
| * Iterate files which are matched by a given glob pattern. | |
| * @param {string} pattern The glob pattern to iterate files. | |
| * @param {boolean} dotfiles If `true` then it doesn't skip dot files by default. | |
| * @returns {IterableIterator<FileEntry>} The found files. | |
| * @private | |
| */ | |
| _iterateFilesWithGlob(pattern, dotfiles) { | |
| debug(`Glob: ${pattern}`); | |
| const { cwd } = internalSlotsMap.get(this); | |
| const directoryPath = path.resolve(cwd, getGlobParent(pattern)); | |
| const absolutePath = path.resolve(cwd, pattern); | |
| const globPart = absolutePath.slice(directoryPath.length + 1); | |
| /* | |
| * recursive if there are `**` or path separators in the glob part. | |
| * Otherwise, patterns such as `src/*.js`, it doesn't need recursive. | |
| */ | |
| const recursive = /\*\*|\/|\\/u.test(globPart); | |
| const selector = new Minimatch(absolutePath, minimatchOpts); | |
| debug(`recursive? ${recursive}`); | |
| return this._iterateFilesRecursive(directoryPath, { | |
| dotfiles, | |
| recursive, | |
| selector, | |
| }); | |
| } | |
| /** | |
| * 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. | |
| * @param {InstanceType<Minimatch>} [options.selector] The matcher to choose files. | |
| * @returns {IterableIterator<FileEntry>} The found files. | |
| * @private | |
| */ | |
| *_iterateFilesRecursive(directoryPath, options) { | |
| debug(`Enter the directory: ${directoryPath}`); | |
| const { configArrayFactory } = internalSlotsMap.get(this); | |
| /** @type {ConfigArray|null} */ | |
| let config = null; | |
| // 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 (!config) { | |
| config = configArrayFactory.getConfigArrayForFile( | |
| filePath, | |
| /* | |
| * We must ignore `ConfigurationNotFoundError` at this | |
| * point because we don't know if target files exist in | |
| * this directory. | |
| */ | |
| { ignoreNotFoundError: true }, | |
| ); | |
| } | |
| const matched = options.selector | |
| ? // Started with a glob pattern; choose by the pattern. | |
| options.selector.match(filePath) | |
| : // Started with a directory path; choose by file extensions. | |
| this.isTargetPath(filePath, config); | |
| if (matched) { | |
| const ignored = this._isIgnoredFile(filePath, { | |
| ...options, | |
| config, | |
| }); | |
| const flag = ignored ? IGNORED_SILENTLY : NONE; | |
| debug( | |
| `Yield: ${entry.name}${ignored ? " but ignored" : ""}`, | |
| ); | |
| yield { | |
| config: configArrayFactory.getConfigArrayForFile( | |
| filePath, | |
| ), | |
| filePath, | |
| flag, | |
| }; | |
| } else { | |
| debug(`Didn't match: ${entry.name}`); | |
| } | |
| // Dive into the sub directory. | |
| } else if (options.recursive && fileInfo.isDirectory()) { | |
| if (!config) { | |
| config = configArrayFactory.getConfigArrayForFile( | |
| filePath, | |
| { ignoreNotFoundError: true }, | |
| ); | |
| } | |
| const ignored = this._isIgnoredFile(filePath + path.sep, { | |
| ...options, | |
| config, | |
| }); | |
| if (!ignored) { | |
| yield* this._iterateFilesRecursive(filePath, options); | |
| } | |
| } | |
| } | |
| debug(`Leave the directory: ${directoryPath}`); | |
| } | |
| /** | |
| * Check if a given file should be ignored. | |
| * @param {string} filePath The path to a file to check. | |
| * @param {Object} options Options | |
| * @param {ConfigArray} [options.config] The config for this file. | |
| * @param {boolean} [options.dotfiles] If `true` then this is not ignore dot files by default. | |
| * @param {boolean} [options.direct] If `true` then this is a direct specified file. | |
| * @returns {boolean} `true` if the file should be ignored. | |
| * @private | |
| */ | |
| _isIgnoredFile( | |
| filePath, | |
| { config: providedConfig, dotfiles = false, direct = false }, | |
| ) { | |
| const { configArrayFactory, defaultIgnores, ignoreFlag } = | |
| internalSlotsMap.get(this); | |
| if (ignoreFlag) { | |
| const config = | |
| providedConfig || | |
| configArrayFactory.getConfigArrayForFile(filePath, { | |
| ignoreNotFoundError: true, | |
| }); | |
| const ignores = | |
| config.extractConfig(filePath).ignores || defaultIgnores; | |
| return ignores(filePath, dotfiles); | |
| } | |
| return !direct && defaultIgnores(filePath, dotfiles); | |
| } | |
| } | |
| //------------------------------------------------------------------------------ | |
| // Public Interface | |
| //------------------------------------------------------------------------------ | |
| module.exports = { FileEnumerator }; | |