%PDF- %PDF-
Direktori : /var/www/html/node_modules/eslint/lib/cli-engine/ |
Current File : /var/www/html/node_modules/eslint/lib/cli-engine/file-enumerator.js |
/** * @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> */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const fs = require("fs"); const path = require("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) { /* 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. * @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) { /* istanbul 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, eslintRecommendedPath: path.resolve(__dirname, "../../conf/eslint-recommended.js"), eslintAllPath: path.resolve(__dirname, "../../conf/eslint-all.js") }), 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(absolutePath, 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 directoryPath = path.resolve(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) { 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 };