|                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |  | /** * @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) {
        /* 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 };
 |