| /** |
| * @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 provies 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> |
| */ |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const fs = require("fs"); |
| const path = require("path"); |
| const getGlobParent = require("glob-parent"); |
| const isGlob = require("is-glob"); |
| const { escapeRegExp } = require("lodash"); |
| const { Minimatch } = require("minimatch"); |
| const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory"); |
| const { IgnoredPaths } = require("./ignored-paths"); |
| 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 {IgnoredPaths} [ignoredPaths] The ignored paths. |
| * @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} 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 {IgnoredPaths} ignoredPathsWithDotfiles The ignored paths but don't include dot files. |
| * @property {IgnoredPaths} ignoredPaths The ignored paths. |
| */ |
| |
| /** @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. |
| * @returns {fs.Stats|null} The stats. |
| * @private |
| */ |
| function statSafeSync(filePath) { |
| try { |
| return fs.statSync(filePath); |
| } catch (error) { |
| /* istanbul 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. |
| * @returns {string[]} The filenames. |
| * @private |
| */ |
| function readdirSafeSync(directoryPath) { |
| try { |
| return fs.readdirSync(directoryPath); |
| } catch (error) { |
| /* istanbul ignore next */ |
| if (error.code !== "ENOENT") { |
| throw error; |
| } |
| return []; |
| } |
| } |
| |
| /** |
| * 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 }), |
| extensions = [".js"], |
| globInputPaths = true, |
| ignore = true, |
| ignoredPaths = new IgnoredPaths({ cwd, ignore }) |
| } = {}) { |
| internalSlotsMap.set(this, { |
| configArrayFactory, |
| cwd, |
| extensionRegExp: new RegExp( |
| `.\\.(?:${extensions |
| .map(ext => escapeRegExp( |
| ext.startsWith(".") |
| ? ext.slice(1) |
| : ext |
| )) |
| .join("|") |
| })$`, |
| "u" |
| ), |
| globInputPaths, |
| ignoreFlag: ignore, |
| ignoredPaths, |
| ignoredPathsWithDotfiles: new IgnoredPaths({ |
| ...ignoredPaths.options, |
| dotfiles: true |
| }) |
| }); |
| } |
| |
| /** |
| * The `RegExp` object that tests if a file path has the allowed file extensions. |
| * @type {RegExp} |
| */ |
| get extensionRegExp() { |
| return internalSlotsMap.get(this).extensionRegExp; |
| } |
| |
| /** |
| * Iterate files which are matched by given glob patterns. |
| * @param {string|string[]} patternOrPatterns The glob patterns to iterate files. |
| * @returns {IterableIterator<FileAndConfig>} The found files. |
| */ |
| *iterateFiles(patternOrPatterns) { |
| const { globInputPaths } = 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 pttern. |
| 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 (!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); |
| |
| if (globInputPaths && isGlobPattern(pattern)) { |
| return this._iterateFilesWithGlob( |
| absolutePath, |
| dotfilesPattern.test(pattern) |
| ); |
| } |
| |
| const stat = statSafeSync(absolutePath); |
| |
| if (stat && stat.isDirectory()) { |
| return this._iterateFilesWithDirectory( |
| absolutePath, |
| dotfilesPattern.test(pattern) |
| ); |
| } |
| |
| if (stat && stat.isFile()) { |
| return this._iterateFilesWithFile(absolutePath); |
| } |
| |
| 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, { 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 directoryPath = getGlobParent(pattern); |
| const globPart = pattern.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(pattern, 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) { |
| if (this._isIgnoredFile(directoryPath + path.sep, options)) { |
| return; |
| } |
| debug(`Enter the directory: ${directoryPath}`); |
| const { configArrayFactory, extensionRegExp } = internalSlotsMap.get(this); |
| |
| /** @type {ConfigArray|null} */ |
| let config = null; |
| |
| // Enumerate the files of this directory. |
| for (const filename of readdirSafeSync(directoryPath)) { |
| const filePath = path.join(directoryPath, filename); |
| const stat = statSafeSync(filePath); // TODO: Use `withFileTypes` in the future. |
| |
| // Check if the file is matched. |
| if (stat && stat.isFile()) { |
| if (!config) { |
| config = configArrayFactory.getConfigArrayForFile(filePath); |
| } |
| const ignored = this._isIgnoredFile(filePath, options); |
| const flag = ignored ? IGNORED_SILENTLY : NONE; |
| 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. |
| : extensionRegExp.test(filePath); |
| |
| if (matched) { |
| debug(`Yield: ${filename}${ignored ? " but ignored" : ""}`); |
| yield { config, filePath, flag }; |
| } else { |
| debug(`Didn't match: ${filename}`); |
| } |
| |
| // Dive into the sub directory. |
| } else if (options.recursive && stat && stat.isDirectory()) { |
| 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 {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, { dotfiles = false, direct = false }) { |
| const { |
| ignoreFlag, |
| ignoredPaths, |
| ignoredPathsWithDotfiles |
| } = internalSlotsMap.get(this); |
| const adoptedIgnoredPaths = dotfiles |
| ? ignoredPathsWithDotfiles |
| : ignoredPaths; |
| |
| return ignoreFlag |
| ? adoptedIgnoredPaths.contains(filePath) |
| : (!direct && adoptedIgnoredPaths.contains(filePath, "default")); |
| } |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Public Interface |
| //------------------------------------------------------------------------------ |
| |
| module.exports = { FileEnumerator }; |