code:
/**
 * @fileoverview Main CLI object.
 * @author Nicholas C. Zakas
 */

"use strict";

/*
 * NOTE: The CLI object should *not* call process.exit() directly. It should only return
 * exit codes. This allows other programs to use the CLI object and still control
 * when the program exits.
 */

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const fs = require("fs"),
    path = require("path"),
    { promisify } = require("util"),
    { ESLint } = require("./eslint"),
    { FlatESLint, shouldUseFlatConfig } = require("./eslint/flat-eslint"),
    createCLIOptions = require("./options"),
    log = require("./shared/logging"),
    RuntimeInfo = require("./shared/runtime-info"),
    { normalizeSeverityToString } = require("./shared/severity");
const { Legacy: { naming } } = require("@eslint/eslintrc");
const { ModuleImporter } = require("@humanwhocodes/module-importer");

const debug = require("debug")("eslint:cli");

//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------

/** @typedef {import("./eslint/eslint").ESLintOptions} ESLintOptions */
/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
/** @typedef {import("./eslint/eslint").LintResult} LintResult */
/** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
/** @typedef {import("./shared/types").ResultsMeta} ResultsMeta */

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

const mkdir = promisify(fs.mkdir);
const stat = promisify(fs.stat);
const writeFile = promisify(fs.writeFile);

/**
 * Predicate function for whether or not to apply fixes in quiet mode.
 * If a message is a warning, do not apply a fix.
 * @param {LintMessage} message The lint result.
 * @returns {boolean} True if the lint message is an error (and thus should be
 * autofixed), false otherwise.
 */
function quietFixPredicate(message) {
    return message.severity === 2;
}

/**
 * Translates the CLI options into the options expected by the ESLint constructor.
 * @param {ParsedCLIOptions} cliOptions The CLI options to translate.
 * @param {"flat"|"eslintrc"} [configType="eslintrc"] The format of the
 *      config to generate.
 * @returns {Promise<ESLintOptions>} The options object for the ESLint constructor.
 * @private
 */
async function translateOptions({
    cache,
    cacheFile,
    cacheLocation,
    cacheStrategy,
    config,
    configLookup,
    env,
    errorOnUnmatchedPattern,
    eslintrc,
    ext,
    fix,
    fixDryRun,
    fixType,
    global,
    ignore,
    ignorePath,
    ignorePattern,
    inlineConfig,
    parser,
    parserOptions,
    plugin,
    quiet,
    reportUnusedDisableDirectives,
    reportUnusedDisableDirectivesSeverity,
    resolvePluginsRelativeTo,
    rule,
    rulesdir,
    warnIgnored
}, configType) {

    let overrideConfig, overrideConfigFile;
    const importer = new ModuleImporter();

    if (configType === "flat") {
        overrideConfigFile = (typeof config === "string") ? config : !configLookup;
        if (overrideConfigFile === false) {
            overrideConfigFile = void 0;
        }

        let globals = {};

        if (global) {
            globals = global.reduce((obj, name) => {
                if (name.endsWith(":true")) {
                    obj[name.slice(0, -5)] = "writable";
                } else {
                    obj[name] = "readonly";
                }
                return obj;
            }, globals);
        }

        overrideConfig = [{
            languageOptions: {
                globals,
                parserOptions: parserOptions || {}
            },
            rules: rule ? rule : {}
        }];

        if (reportUnusedDisableDirectives || reportUnusedDisableDirectivesSeverity !== void 0) {
            overrideConfig[0].linterOptions = {
                reportUnusedDisableDirectives: reportUnusedDisableDirectives
                    ? "error"
                    : normalizeSeverityToString(reportUnusedDisableDirectivesSeverity)
            };
        }

        if (parser) {
            overrideConfig[0].languageOptions.parser = await importer.import(parser);
        }

        if (plugin) {
            const plugins = {};

            for (const pluginName of plugin) {

                const shortName = naming.getShorthandName(pluginName, "eslint-plugin");
                const longName = naming.normalizePackageName(pluginName, "eslint-plugin");

                plugins[shortName] = await importer.import(longName);
            }

            overrideConfig[0].plugins = plugins;
        }

    } else {
        overrideConfigFile = config;

        overrideConfig = {
            env: env && env.reduce((obj, name) => {
                obj[name] = true;
                return obj;
            }, {}),
            globals: global && global.reduce((obj, name) => {
                if (name.endsWith(":true")) {
                    obj[name.slice(0, -5)] = "writable";
                } else {
                    obj[name] = "readonly";
                }
                return obj;
            }, {}),
            ignorePatterns: ignorePattern,
            parser,
            parserOptions,
            plugins: plugin,
            rules: rule
        };
    }

    const options = {
        allowInlineConfig: inlineConfig,
        cache,
        cacheLocation: cacheLocation || cacheFile,
        cacheStrategy,
        errorOnUnmatchedPattern,
        fix: (fix || fixDryRun) && (quiet ? quietFixPredicate : true),
        fixTypes: fixType,
        ignore,
        overrideConfig,
        overrideConfigFile
    };

    if (configType === "flat") {
        options.ignorePatterns = ignorePattern;
        options.warnIgnored = warnIgnored;
    } else {
        options.resolvePluginsRelativeTo = resolvePluginsRelativeTo;
        options.rulePaths = rulesdir;
        options.useEslintrc = eslintrc;
        options.extensions = ext;
        options.ignorePath = ignorePath;
        if (reportUnusedDisableDirectives || reportUnusedDisableDirectivesSeverity !== void 0) {
            options.reportUnusedDisableDirectives = reportUnusedDisableDirectives
                ? "error"
                : normalizeSeverityToString(reportUnusedDisableDirectivesSeverity);
        }
    }

    return options;
}

/**
 * Count error messages.
 * @param {LintResult[]} results The lint results.
 * @returns {{errorCount:number;fatalErrorCount:number,warningCount:number}} The number of error messages.
 */
function countErrors(results) {
    let errorCount = 0;
    let fatalErrorCount = 0;
    let warningCount = 0;

    for (const result of results) {
        errorCount += result.errorCount;
        fatalErrorCount += result.fatalErrorCount;
        warningCount += result.warningCount;
    }

    return { errorCount, fatalErrorCount, warningCount };
}

/**
 * Check if a given file path is a directory or not.
 * @param {string} filePath The path to a file to check.
 * @returns {Promise<boolean>} `true` if the given path is a directory.
 */
async function isDirectory(filePath) {
    try {
        return (await stat(filePath)).isDirectory();
    } catch (error) {
        if (error.code === "ENOENT" || error.code === "ENOTDIR") {
            return false;
        }
        throw error;
    }
}

/**
 * Outputs the results of the linting.
 * @param {ESLint} engine The ESLint instance to use.
 * @param {LintResult[]} results The results to print.
 * @param {string} format The name of the formatter to use or the path to the formatter.
 * @param {string} outputFile The path for the output file.
 * @param {ResultsMeta} resultsMeta Warning count and max threshold.
 * @returns {Promise<boolean>} True if the printing succeeds, false if not.
 * @private
 */
async function printResults(engine, results, format, outputFile, resultsMeta) {
    let formatter;

    try {
        formatter = await engine.loadFormatter(format);
    } catch (e) {
        log.error(e.message);
        return false;
    }

    const output = await formatter.format(results, resultsMeta);

    if (output) {
        if (outputFile) {
            const filePath = path.resolve(process.cwd(), outputFile);

            if (await isDirectory(filePath)) {
                log.error("Cannot write to output file path, it is a directory: %s", outputFile);
                return false;
            }

            try {
                await mkdir(path.dirname(filePath), { recursive: true });
                await writeFile(filePath, output);
            } catch (ex) {
                log.error("There was a problem writing the output file:\n%s", ex);
                return false;
            }
        } else {
            log.info(output);
        }
    }

    return true;
}

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------

/**
 * Encapsulates all CLI behavior for eslint. Makes it easier to test as well as
 * for other Node.js programs to effectively run the CLI.
 */
const cli = {

    /**
     * Executes the CLI based on an array of arguments that is passed in.
     * @param {string|Array|Object} args The arguments to process.
     * @param {string} [text] The text to lint (used for TTY).
     * @param {boolean} [allowFlatConfig] Whether or not to allow flat config.
     * @returns {Promise<number>} The exit code for the operation.
     */
    async execute(args, text, allowFlatConfig) {
        if (Array.isArray(args)) {
            debug("CLI args: %o", args.slice(2));
        }

        /*
         * Before doing anything, we need to see if we are using a
         * flat config file. If so, then we need to change the way command
         * line args are parsed. This is temporary, and when we fully
         * switch to flat config we can remove this logic.
         */

        const usingFlatConfig = allowFlatConfig && await shouldUseFlatConfig();

        debug("Using flat config?", usingFlatConfig);

        const CLIOptions = createCLIOptions(usingFlatConfig);

        /** @type {ParsedCLIOptions} */
        let options;

        try {
            options = CLIOptions.parse(args);
        } catch (error) {
            debug("Error parsing CLI options:", error.message);

            let errorMessage = error.message;

            if (usingFlatConfig) {
                errorMessage += "\nYou're using eslint.config.js, some command line flags are no longer available. Please see https://eslint.org/docs/latest/use/command-line-interface for details.";
            }

            log.error(errorMessage);
            return 2;
        }

        const files = options._;
        const useStdin = typeof text === "string";

        if (options.help) {
            log.info(CLIOptions.generateHelp());
            return 0;
        }
        if (options.version) {
            log.info(RuntimeInfo.version());
            return 0;
        }
        if (options.envInfo) {
            try {
                log.info(RuntimeInfo.environment());
                return 0;
            } catch (err) {
                debug("Error retrieving environment info");
                log.error(err.message);
                return 2;
            }
        }

        if (options.printConfig) {
            if (files.length) {
                log.error("The --print-config option must be used with exactly one file name.");
                return 2;
            }
            if (useStdin) {
                log.error("The --print-config option is not available for piped-in code.");
                return 2;
            }

            const engine = usingFlatConfig
                ? new FlatESLint(await translateOptions(options, "flat"))
                : new ESLint(await translateOptions(options));
            const fileConfig =
                await engine.calculateConfigForFile(options.printConfig);

            log.info(JSON.stringify(fileConfig, null, "  "));
            return 0;
        }

        debug(`Running on ${useStdin ? "text" : "files"}`);

        if (options.fix && options.fixDryRun) {
            log.error("The --fix option and the --fix-dry-run option cannot be used together.");
            return 2;
        }
        if (useStdin && options.fix) {
            log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead.");
            return 2;
        }
        if (options.fixType && !options.fix && !options.fixDryRun) {
            log.error("The --fix-type option requires either --fix or --fix-dry-run.");
            return 2;
        }

        if (options.reportUnusedDisableDirectives && options.reportUnusedDisableDirectivesSeverity !== void 0) {
            log.error("The --report-unused-disable-directives option and the --report-unused-disable-directives-severity option cannot be used together.");
            return 2;
        }

        const ActiveESLint = usingFlatConfig ? FlatESLint : ESLint;

        const engine = new ActiveESLint(await translateOptions(options, usingFlatConfig ? "flat" : "eslintrc"));
        let results;

        if (useStdin) {
            results = await engine.lintText(text, {
                filePath: options.stdinFilename,

                // flatConfig respects CLI flag and constructor warnIgnored, eslintrc forces true for backwards compatibility
                warnIgnored: usingFlatConfig ? void 0 : true
            });
        } else {
            results = await engine.lintFiles(files);
        }

        if (options.fix) {
            debug("Fix mode enabled - applying fixes");
            await ActiveESLint.outputFixes(results);
        }

        let resultsToPrint = results;

        if (options.quiet) {
            debug("Quiet mode enabled - filtering out warnings");
            resultsToPrint = ActiveESLint.getErrorResults(resultsToPrint);
        }

        const resultCounts = countErrors(results);
        const tooManyWarnings = options.maxWarnings >= 0 && resultCounts.warningCount > options.maxWarnings;
        const resultsMeta = tooManyWarnings
            ? {
                maxWarningsExceeded: {
                    maxWarnings: options.maxWarnings,
                    foundWarnings: resultCounts.warningCount
                }
            }
            : {};

        if (await printResults(engine, resultsToPrint, options.format, options.outputFile, resultsMeta)) {

            // Errors and warnings from the original unfiltered results should determine the exit code
            const shouldExitForFatalErrors =
                options.exitOnFatalError && resultCounts.fatalErrorCount > 0;

            if (!resultCounts.errorCount && tooManyWarnings) {
                log.error(
                    "ESLint found too many warnings (maximum: %s).",
                    options.maxWarnings
                );
            }

            if (shouldExitForFatalErrors) {
                return 2;
            }

            return (resultCounts.errorCount || tooManyWarnings) ? 1 : 0;
        }

        return 2;
    }
};

module.exports = cli;


code:
/**
 * @fileoverview Default configuration
 * @author Nicholas C. Zakas
 */

"use strict";

//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------

const Rules = require("../rules");

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

exports.defaultConfig = [
    {
        plugins: {
            "@": {

                /*
                 * Because we try to delay loading rules until absolutely
                 * necessary, a proxy allows us to hook into the lazy-loading
                 * aspect of the rules map while still keeping all of the
                 * relevant configuration inside of the config array.
                 */
                rules: new Proxy({}, {
                    get(target, property) {
                        return Rules.get(property);
                    },

                    has(target, property) {
                        return Rules.has(property);
                    }
                })
            }
        },
        languageOptions: {
            sourceType: "module",
            ecmaVersion: "latest",
            parser: require("espree"),
            parserOptions: {}
        }
    },

    // default ignores are listed here
    {
        ignores: [
            "**/node_modules/",
            ".git/"
        ]
    },

    // intentionally empty config to ensure these files are globbed by default
    {
        files: ["**/*.js", "**/*.mjs"]
    },
    {
        files: ["**/*.cjs"],
        languageOptions: {
            sourceType: "commonjs",
            ecmaVersion: "latest"
        }
    }
];

license: mit


code:
/**
 * @fileoverview Flat Config Array
 * @author Nicholas C. Zakas
 */

"use strict";

//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------

const { ConfigArray, ConfigArraySymbol } = require("@humanwhocodes/config-array");
const { flatConfigSchema } = require("./flat-config-schema");
const { RuleValidator } = require("./rule-validator");
const { defaultConfig } = require("./default-config");
const jsPlugin = require("@eslint/js");

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

const ruleValidator = new RuleValidator();

/**
 * Splits a plugin identifier in the form a/b/c into two parts: a/b and c.
 * @param {string} identifier The identifier to parse.
 * @returns {{objectName: string, pluginName: string}} The parts of the plugin
 *      name.
 */
function splitPluginIdentifier(identifier) {
    const parts = identifier.split("/");

    return {
        objectName: parts.pop(),
        pluginName: parts.join("/")
    };
}

/**
 * Returns the name of an object in the config by reading its `meta` key.
 * @param {Object} object The object to check.
 * @returns {string?} The name of the object if found or `null` if there
 *      is no name.
 */
function getObjectId(object) {

    // first check old-style name
    let name = object.name;

    if (!name) {

        if (!object.meta) {
            return null;
        }

        name = object.meta.name;

        if (!name) {
            return null;
        }
    }

    // now check for old-style version
    let version = object.version;

    if (!version) {
        version = object.meta && object.meta.version;
    }

    // if there's a version then append that
    if (version) {
        return `${name}@${version}`;
    }

    return name;
}

const originalBaseConfig = Symbol("originalBaseConfig");

//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------

/**
 * Represents an array containing configuration information for ESLint.
 */
class FlatConfigArray extends ConfigArray {

    /**
     * Creates a new instance.
     * @param {*[]} configs An array of configuration information.
     * @param {{basePath: string, shouldIgnore: boolean, baseConfig: FlatConfig}} options The options
     *      to use for the config array instance.
     */
    constructor(configs, {
        basePath,
        shouldIgnore = true,
        baseConfig = defaultConfig
    } = {}) {
        super(configs, {
            basePath,
            schema: flatConfigSchema
        });

        if (baseConfig[Symbol.iterator]) {
            this.unshift(...baseConfig);
        } else {
            this.unshift(baseConfig);
        }

        /**
         * The base config used to build the config array.
         * @type {Array<FlatConfig>}
         */
        this[originalBaseConfig] = baseConfig;
        Object.defineProperty(this, originalBaseConfig, { writable: false });

        /**
         * Determines if `ignores` fields should be honored.
         * If true, then all `ignores` fields are honored.
         * if false, then only `ignores` fields in the baseConfig are honored.
         * @type {boolean}
         */
        this.shouldIgnore = shouldIgnore;
        Object.defineProperty(this, "shouldIgnore", { writable: false });
    }

    /* eslint-disable class-methods-use-this -- Desired as instance method */
    /**
     * Replaces a config with another config to allow us to put strings
     * in the config array that will be replaced by objects before
     * normalization.
     * @param {Object} config The config to preprocess.
     * @returns {Object} The preprocessed config.
     */
    [ConfigArraySymbol.preprocessConfig](config) {
        if (config === "eslint:recommended") {

            // if we are in a Node.js environment warn the user
            if (typeof process !== "undefined" && process.emitWarning) {
                process.emitWarning("The 'eslint:recommended' string configuration is deprecated and will be replaced by the @eslint/js package's 'recommended' config.");
            }

            return jsPlugin.configs.recommended;
        }

        if (config === "eslint:all") {

            // if we are in a Node.js environment warn the user
            if (typeof process !== "undefined" && process.emitWarning) {
                process.emitWarning("The 'eslint:all' string configuration is deprecated and will be replaced by the @eslint/js package's 'all' config.");
            }

            return jsPlugin.configs.all;
        }

        /*
         * If `shouldIgnore` is false, we remove any ignore patterns specified
         * in the config so long as it's not a default config and it doesn't
         * have a `files` entry.
         */
        if (
            !this.shouldIgnore &&
            !this[originalBaseConfig].includes(config) &&
            config.ignores &&
            !config.files
        ) {
            /* eslint-disable-next-line no-unused-vars -- need to strip off other keys */
            const { ignores, ...otherKeys } = config;

            return otherKeys;
        }

        return config;
    }

    /**
     * Finalizes the config by replacing plugin references with their objects
     * and validating rule option schemas.
     * @param {Object} config The config to finalize.
     * @returns {Object} The finalized config.
     * @throws {TypeError} If the config is invalid.
     */
    [ConfigArraySymbol.finalizeConfig](config) {

        const { plugins, languageOptions, processor } = config;
        let parserName, processorName;
        let invalidParser = false,
            invalidProcessor = false;

        // Check parser value
        if (languageOptions && languageOptions.parser) {
            const { parser } = languageOptions;

            if (typeof parser === "object") {
                parserName = getObjectId(parser);

                if (!parserName) {
                    invalidParser = true;
                }

            } else {
                invalidParser = true;
            }
        }

        // Check processor value
        if (processor) {
            if (typeof processor === "string") {
                const { pluginName, objectName: localProcessorName } = splitPluginIdentifier(processor);

                processorName = processor;

                if (!plugins || !plugins[pluginName] || !plugins[pluginName].processors || !plugins[pluginName].processors[localProcessorName]) {
                    throw new TypeError(`Key "processor": Could not find "${localProcessorName}" in plugin "${pluginName}".`);
                }

                config.processor = plugins[pluginName].processors[localProcessorName];
            } else if (typeof processor === "object") {
                processorName = getObjectId(processor);

                if (!processorName) {
                    invalidProcessor = true;
                }

            } else {
                invalidProcessor = true;
            }
        }

        ruleValidator.validate(config);

        // apply special logic for serialization into JSON
        /* eslint-disable object-shorthand -- shorthand would change "this" value */
        Object.defineProperty(config, "toJSON", {
            value: function() {

                if (invalidParser) {
                    throw new Error("Could not serialize parser object (missing 'meta' object).");
                }

                if (invalidProcessor) {
                    throw new Error("Could not serialize processor object (missing 'meta' object).");
                }

                return {
                    ...this,
                    plugins: Object.entries(plugins).map(([namespace, plugin]) => {

                        const pluginId = getObjectId(plugin);

                        if (!pluginId) {
                            return namespace;
                        }

                        return `${namespace}:${pluginId}`;
                    }),
                    languageOptions: {
                        ...languageOptions,
                        parser: parserName
                    },
                    processor: processorName
                };
            }
        });
        /* eslint-enable object-shorthand -- ok to enable now */

        return config;
    }
    /* eslint-enable class-methods-use-this -- Desired as instance method */

}

exports.FlatConfigArray = FlatConfigArray;


code:
/**
 * @fileoverview Shared functions to work with configs.
 * @author Nicholas C. Zakas
 */

"use strict";

//-----------------------------------------------------------------------------
// Functions
//-----------------------------------------------------------------------------

/**
 * Parses a ruleId into its plugin and rule parts.
 * @param {string} ruleId The rule ID to parse.
 * @returns {{pluginName:string,ruleName:string}} The plugin and rule
 *      parts of the ruleId;
 */
function parseRuleId(ruleId) {
    let pluginName, ruleName;

    // distinguish between core rules and plugin rules
    if (ruleId.includes("/")) {

        // mimic scoped npm packages
        if (ruleId.startsWith("@")) {
            pluginName = ruleId.slice(0, ruleId.lastIndexOf("/"));
        } else {
            pluginName = ruleId.slice(0, ruleId.indexOf("/"));
        }

        ruleName = ruleId.slice(pluginName.length + 1);
    } else {
        pluginName = "@";
        ruleName = ruleId;
    }

    return {
        pluginName,
        ruleName
    };
}

/**
 * Retrieves a rule instance from a given config based on the ruleId.
 * @param {string} ruleId The rule ID to look for.
 * @param {FlatConfig} config The config to search.
 * @returns {import("../shared/types").Rule|undefined} The rule if found
 *      or undefined if not.
 */
function getRuleFromConfig(ruleId, config) {

    const { pluginName, ruleName } = parseRuleId(ruleId);

    const plugin = config.plugins && config.plugins[pluginName];
    let rule = plugin && plugin.rules && plugin.rules[ruleName];

    // normalize function rules into objects
    if (rule && typeof rule === "function") {
        rule = {
            create: rule
        };
    }

    return rule;
}

/**
 * Gets a complete options schema for a rule.
 * @param {{create: Function, schema: (Array|null)}} rule A new-style rule object
 * @returns {Object} JSON Schema for the rule's options.
 */
function getRuleOptionsSchema(rule) {

    if (!rule) {
        return null;
    }

    const schema = rule.schema || rule.meta && rule.meta.schema;

    if (Array.isArray(schema)) {
        if (schema.length) {
            return {
                type: "array",
                items: schema,
                minItems: 0,
                maxItems: schema.length
            };
        }
        return {
            type: "array",
            minItems: 0,
            maxItems: 0
        };

    }

    // Given a full schema, leave it alone
    return schema || null;
}

//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------

module.exports = {
    parseRuleId,
    getRuleFromConfig,
    getRuleOptionsSchema
};


code:
/**
 * @fileoverview Flat config schema
 * @author Nicholas C. Zakas
 */

"use strict";

//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------

/*
 * Note: This can be removed in ESLint v9 because structuredClone is available globally
 * starting in Node.js v17.
 */
const structuredClone = require("@ungap/structured-clone").default;
const { normalizeSeverityToNumber } = require("../shared/severity");

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/**
 * @typedef ObjectPropertySchema
 * @property {Function|string} merge The function or name of the function to call
 *      to merge multiple objects with this property.
 * @property {Function|string} validate The function or name of the function to call
 *      to validate the value of this property.
 */

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

const ruleSeverities = new Map([
    [0, 0], ["off", 0],
    [1, 1], ["warn", 1],
    [2, 2], ["error", 2]
]);

const globalVariablesValues = new Set([
    true, "true", "writable", "writeable",
    false, "false", "readonly", "readable", null,
    "off"
]);

/**
 * Check if a value is a non-null object.
 * @param {any} value The value to check.
 * @returns {boolean} `true` if the value is a non-null object.
 */
function isNonNullObject(value) {
    return typeof value === "object" && value !== null;
}

/**
 * Check if a value is a non-null non-array object.
 * @param {any} value The value to check.
 * @returns {boolean} `true` if the value is a non-null non-array object.
 */
function isNonArrayObject(value) {
    return isNonNullObject(value) && !Array.isArray(value);
}

/**
 * Check if a value is undefined.
 * @param {any} value The value to check.
 * @returns {boolean} `true` if the value is undefined.
 */
function isUndefined(value) {
    return typeof value === "undefined";
}

/**
 * Deeply merges two non-array objects.
 * @param {Object} first The base object.
 * @param {Object} second The overrides object.
 * @param {Map<string, Map<string, Object>>} [mergeMap] Maps the combination of first and second arguments to a merged result.
 * @returns {Object} An object with properties from both first and second.
 */
function deepMerge(first, second, mergeMap = new Map()) {

    let secondMergeMap = mergeMap.get(first);

    if (secondMergeMap) {
        const result = secondMergeMap.get(second);

        if (result) {

            // If this combination of first and second arguments has been already visited, return the previously created result.
            return result;
        }
    } else {
        secondMergeMap = new Map();
        mergeMap.set(first, secondMergeMap);
    }

    /*
     * First create a result object where properties from the second object
     * overwrite properties from the first. This sets up a baseline to use
     * later rather than needing to inspect and change every property
     * individually.
     */
    const result = {
        ...first,
        ...second
    };

    delete result.__proto__; // eslint-disable-line no-proto -- don't merge own property "__proto__"

    // Store the pending result for this combination of first and second arguments.
    secondMergeMap.set(second, result);

    for (const key of Object.keys(second)) {

        // avoid hairy edge case
        if (key === "__proto__" || !Object.prototype.propertyIsEnumerable.call(first, key)) {
            continue;
        }

        const firstValue = first[key];
        const secondValue = second[key];

        if (isNonArrayObject(firstValue) && isNonArrayObject(secondValue)) {
            result[key] = deepMerge(firstValue, secondValue, mergeMap);
        } else if (isUndefined(secondValue)) {
            result[key] = firstValue;
        }
    }

    return result;

}

/**
 * Normalizes the rule options config for a given rule by ensuring that
 * it is an array and that the first item is 0, 1, or 2.
 * @param {Array|string|number} ruleOptions The rule options config.
 * @returns {Array} An array of rule options.
 */
function normalizeRuleOptions(ruleOptions) {

    const finalOptions = Array.isArray(ruleOptions)
        ? ruleOptions.slice(0)
        : [ruleOptions];

    finalOptions[0] = ruleSeverities.get(finalOptions[0]);
    return structuredClone(finalOptions);
}

//-----------------------------------------------------------------------------
// Assertions
//-----------------------------------------------------------------------------

/**
 * The error type when a rule's options are configured with an invalid type.
 */
class InvalidRuleOptionsError extends Error {

    /**
     * @param {string} ruleId Rule name being configured.
     * @param {any} value The invalid value.
     */
    constructor(ruleId, value) {
        super(`Key "${ruleId}": Expected severity of "off", 0, "warn", 1, "error", or 2.`);
        this.messageTemplate = "invalid-rule-options";
        this.messageData = { ruleId, value };
    }
}

/**
 * Validates that a value is a valid rule options entry.
 * @param {string} ruleId Rule name being configured.
 * @param {any} value The value to check.
 * @returns {void}
 * @throws {InvalidRuleOptionsError} If the value isn't a valid rule options.
 */
function assertIsRuleOptions(ruleId, value) {
    if (typeof value !== "string" && typeof value !== "number" && !Array.isArray(value)) {
        throw new InvalidRuleOptionsError(ruleId, value);
    }
}

/**
 * The error type when a rule's severity is invalid.
 */
class InvalidRuleSeverityError extends Error {

    /**
     * @param {string} ruleId Rule name being configured.
     * @param {any} value The invalid value.
     */
    constructor(ruleId, value) {
        super(`Key "${ruleId}": Expected severity of "off", 0, "warn", 1, "error", or 2.`);
        this.messageTemplate = "invalid-rule-severity";
        this.messageData = { ruleId, value };
    }
}

/**
 * Validates that a value is valid rule severity.
 * @param {string} ruleId Rule name being configured.
 * @param {any} value The value to check.
 * @returns {void}
 * @throws {InvalidRuleSeverityError} If the value isn't a valid rule severity.
 */
function assertIsRuleSeverity(ruleId, value) {
    const severity = ruleSeverities.get(value);

    if (typeof severity === "undefined") {
        throw new InvalidRuleSeverityError(ruleId, value);
    }
}

/**
 * Validates that a given string is the form pluginName/objectName.
 * @param {string} value The string to check.
 * @returns {void}
 * @throws {TypeError} If the string isn't in the correct format.
 */
function assertIsPluginMemberName(value) {
    if (!/[@a-z0-9-_$]+(?:\/(?:[a-z0-9-_$]+))+$/iu.test(value)) {
        throw new TypeError(`Expected string in the form "pluginName/objectName" but found "${value}".`);
    }
}

/**
 * Validates that a value is an object.
 * @param {any} value The value to check.
 * @returns {void}
 * @throws {TypeError} If the value isn't an object.
 */
function assertIsObject(value) {
    if (!isNonNullObject(value)) {
        throw new TypeError("Expected an object.");
    }
}

/**
 * The error type when there's an eslintrc-style options in a flat config.
 */
class IncompatibleKeyError extends Error {

    /**
     * @param {string} key The invalid key.
     */
    constructor(key) {
        super("This appears to be in eslintrc format rather than flat config format.");
        this.messageTemplate = "eslintrc-incompat";
        this.messageData = { key };
    }
}

/**
 * The error type when there's an eslintrc-style plugins array found.
 */
class IncompatiblePluginsError extends Error {

    /**
     * Creates a new instance.
     * @param {Array<string>} plugins The plugins array.
     */
    constructor(plugins) {
        super("This appears to be in eslintrc format (array of strings) rather than flat config format (object).");
        this.messageTemplate = "eslintrc-plugins";
        this.messageData = { plugins };
    }
}

//-----------------------------------------------------------------------------
// Low-Level Schemas
//-----------------------------------------------------------------------------

/** @type {ObjectPropertySchema} */
const booleanSchema = {
    merge: "replace",
    validate: "boolean"
};

const ALLOWED_SEVERITIES = new Set(["error", "warn", "off", 2, 1, 0]);

/** @type {ObjectPropertySchema} */
const disableDirectiveSeveritySchema = {
    merge(first, second) {
        const value = second === void 0 ? first : second;

        if (typeof value === "boolean") {
            return value ? "warn" : "off";
        }

        return normalizeSeverityToNumber(value);
    },
    validate(value) {
        if (!(ALLOWED_SEVERITIES.has(value) || typeof value === "boolean")) {
            throw new TypeError("Expected one of: \"error\", \"warn\", \"off\", 0, 1, 2, or a boolean.");
        }
    }
};

/** @type {ObjectPropertySchema} */
const deepObjectAssignSchema = {
    merge(first = {}, second = {}) {
        return deepMerge(first, second);
    },
    validate: "object"
};

//-----------------------------------------------------------------------------
// High-Level Schemas
//-----------------------------------------------------------------------------

/** @type {ObjectPropertySchema} */
const globalsSchema = {
    merge: "assign",
    validate(value) {

        assertIsObject(value);

        for (const key of Object.keys(value)) {

            // avoid hairy edge case
            if (key === "__proto__") {
                continue;
            }

            if (key !== key.trim()) {
                throw new TypeError(`Global "${key}" has leading or trailing whitespace.`);
            }

            if (!globalVariablesValues.has(value[key])) {
                throw new TypeError(`Key "${key}": Expected "readonly", "writable", or "off".`);
            }
        }
    }
};

/** @type {ObjectPropertySchema} */
const parserSchema = {
    merge: "replace",
    validate(value) {

        if (!value || typeof value !== "object" ||
            (typeof value.parse !== "function" && typeof value.parseForESLint !== "function")
        ) {
            throw new TypeError("Expected object with parse() or parseForESLint() method.");
        }

    }
};

/** @type {ObjectPropertySchema} */
const pluginsSchema = {
    merge(first = {}, second = {}) {
        const keys = new Set([...Object.keys(first), ...Object.keys(second)]);
        const result = {};

        // manually validate that plugins are not redefined
        for (const key of keys) {

            // avoid hairy edge case
            if (key === "__proto__") {
                continue;
            }

            if (key in first && key in second && first[key] !== second[key]) {
                throw new TypeError(`Cannot redefine plugin "${key}".`);
            }

            result[key] = second[key] || first[key];
        }

        return result;
    },
    validate(value) {

        // first check the value to be sure it's an object
        if (value === null || typeof value !== "object") {
            throw new TypeError("Expected an object.");
        }

        // make sure it's not an array, which would mean eslintrc-style is used
        if (Array.isArray(value)) {
            throw new IncompatiblePluginsError(value);
        }

        // second check the keys to make sure they are objects
        for (const key of Object.keys(value)) {

            // avoid hairy edge case
            if (key === "__proto__") {
                continue;
            }

            if (value[key] === null || typeof value[key] !== "object") {
                throw new TypeError(`Key "${key}": Expected an object.`);
            }
        }
    }
};

/** @type {ObjectPropertySchema} */
const processorSchema = {
    merge: "replace",
    validate(value) {
        if (typeof value === "string") {
            assertIsPluginMemberName(value);
        } else if (value && typeof value === "object") {
            if (typeof value.preprocess !== "function" || typeof value.postprocess !== "function") {
                throw new TypeError("Object must have a preprocess() and a postprocess() method.");
            }
        } else {
            throw new TypeError("Expected an object or a string.");
        }
    }
};

/** @type {ObjectPropertySchema} */
const rulesSchema = {
    merge(first = {}, second = {}) {

        const result = {
            ...first,
            ...second
        };

        for (const ruleId of Object.keys(result)) {

            try {

                // avoid hairy edge case
                if (ruleId === "__proto__") {

                    /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
                    delete result.__proto__;
                    continue;
                }

                result[ruleId] = normalizeRuleOptions(result[ruleId]);

                /*
                 * If either rule config is missing, then the correct
                 * config is already present and we just need to normalize
                 * the severity.
                 */
                if (!(ruleId in first) || !(ruleId in second)) {
                    continue;
                }

                const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
                const secondRuleOptions = normalizeRuleOptions(second[ruleId]);

                /*
                 * If the second rule config only has a severity (length of 1),
                 * then use that severity and keep the rest of the options from
                 * the first rule config.
                 */
                if (secondRuleOptions.length === 1) {
                    result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
                    continue;
                }

                /*
                 * In any other situation, then the second rule config takes
                 * precedence. That means the value at `result[ruleId]` is
                 * already correct and no further work is necessary.
                 */
            } catch (ex) {
                throw new Error(`Key "${ruleId}": ${ex.message}`, { cause: ex });
            }

        }

        return result;

    },

    validate(value) {
        assertIsObject(value);

        /*
         * We are not checking the rule schema here because there is no
         * guarantee that the rule definition is present at this point. Instead
         * we wait and check the rule schema during the finalization step
         * of calculating a config.
         */
        for (const ruleId of Object.keys(value)) {

            // avoid hairy edge case
            if (ruleId === "__proto__") {
                continue;
            }

            const ruleOptions = value[ruleId];

            assertIsRuleOptions(ruleId, ruleOptions);

            if (Array.isArray(ruleOptions)) {
                assertIsRuleSeverity(ruleId, ruleOptions[0]);
            } else {
                assertIsRuleSeverity(ruleId, ruleOptions);
            }
        }
    }
};

/** @type {ObjectPropertySchema} */
const ecmaVersionSchema = {
    merge: "replace",
    validate(value) {
        if (typeof value === "number" || value === "latest") {
            return;
        }

        throw new TypeError("Expected a number or \"latest\".");
    }
};

/** @type {ObjectPropertySchema} */
const sourceTypeSchema = {
    merge: "replace",
    validate(value) {
        if (typeof value !== "string" || !/^(?:script|module|commonjs)$/u.test(value)) {
            throw new TypeError("Expected \"script\", \"module\", or \"commonjs\".");
        }
    }
};

/**
 * Creates a schema that always throws an error. Useful for warning
 * about eslintrc-style keys.
 * @param {string} key The eslintrc key to create a schema for.
 * @returns {ObjectPropertySchema} The schema.
 */
function createEslintrcErrorSchema(key) {
    return {
        merge: "replace",
        validate() {
            throw new IncompatibleKeyError(key);
        }
    };
}

const eslintrcKeys = [
    "env",
    "extends",
    "globals",
    "ignorePatterns",
    "noInlineConfig",
    "overrides",
    "parser",
    "parserOptions",
    "reportUnusedDisableDirectives",
    "root"
];

//-----------------------------------------------------------------------------
// Full schema
//-----------------------------------------------------------------------------

const flatConfigSchema = {

    // eslintrc-style keys that should always error
    ...Object.fromEntries(eslintrcKeys.map(key => [key, createEslintrcErrorSchema(key)])),

    // flat config keys
    settings: deepObjectAssignSchema,
    linterOptions: {
        schema: {
            noInlineConfig: booleanSchema,
            reportUnusedDisableDirectives: disableDirectiveSeveritySchema
        }
    },
    languageOptions: {
        schema: {
            ecmaVersion: ecmaVersionSchema,
            sourceType: sourceTypeSchema,
            globals: globalsSchema,
            parser: parserSchema,
            parserOptions: deepObjectAssignSchema
        }
    },
    processor: processorSchema,
    plugins: pluginsSchema,
    rules: rulesSchema
};

//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------

module.exports = {
    flatConfigSchema,
    assertIsRuleSeverity,
    assertIsRuleOptions
};


code:
/**
 * @fileoverview Rule Validator
 * @author Nicholas C. Zakas
 */

"use strict";

//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------

const ajvImport = require("../shared/ajv");
const ajv = ajvImport();
const {
    parseRuleId,
    getRuleFromConfig,
    getRuleOptionsSchema
} = require("./flat-config-helpers");
const ruleReplacements = require("../../conf/replacements.json");

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

/**
 * Throws a helpful error when a rule cannot be found.
 * @param {Object} ruleId The rule identifier.
 * @param {string} ruleId.pluginName The ID of the rule to find.
 * @param {string} ruleId.ruleName The ID of the rule to find.
 * @param {Object} config The config to search in.
 * @throws {TypeError} For missing plugin or rule.
 * @returns {void}
 */
function throwRuleNotFoundError({ pluginName, ruleName }, config) {

    const ruleId = pluginName === "@" ? ruleName : `${pluginName}/${ruleName}`;

    const errorMessageHeader = `Key "rules": Key "${ruleId}"`;
    let errorMessage = `${errorMessageHeader}: Could not find plugin "${pluginName}".`;

    // if the plugin exists then we need to check if the rule exists
    if (config.plugins && config.plugins[pluginName]) {
        const replacementRuleName = ruleReplacements.rules[ruleName];

        if (pluginName === "@" && replacementRuleName) {

            errorMessage = `${errorMessageHeader}: Rule "${ruleName}" was removed and replaced by "${replacementRuleName}".`;

        } else {

            errorMessage = `${errorMessageHeader}: Could not find "${ruleName}" in plugin "${pluginName}".`;

            // otherwise, let's see if we can find the rule name elsewhere
            for (const [otherPluginName, otherPlugin] of Object.entries(config.plugins)) {
                if (otherPlugin.rules && otherPlugin.rules[ruleName]) {
                    errorMessage += ` Did you mean "${otherPluginName}/${ruleName}"?`;
                    break;
                }
            }

        }

        // falls through to throw error
    }

    throw new TypeError(errorMessage);
}

//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------

/**
 * Implements validation functionality for the rules portion of a config.
 */
class RuleValidator {

    /**
     * Creates a new instance.
     */
    constructor() {

        /**
         * A collection of compiled validators for rules that have already
         * been validated.
         * @type {WeakMap}
         */
        this.validators = new WeakMap();
    }

    /**
     * Validates all of the rule configurations in a config against each
     * rule's schema.
     * @param {Object} config The full config to validate. This object must
     *      contain both the rules section and the plugins section.
     * @returns {void}
     * @throws {Error} If a rule's configuration does not match its schema.
     */
    validate(config) {

        if (!config.rules) {
            return;
        }

        for (const [ruleId, ruleOptions] of Object.entries(config.rules)) {

            // check for edge case
            if (ruleId === "__proto__") {
                continue;
            }

            /*
             * If a rule is disabled, we don't do any validation. This allows
             * users to safely set any value to 0 or "off" without worrying
             * that it will cause a validation error.
             *
             * Note: ruleOptions is always an array at this point because
             * this validation occurs after FlatConfigArray has merged and
             * normalized values.
             */
            if (ruleOptions[0] === 0) {
                continue;
            }

            const rule = getRuleFromConfig(ruleId, config);

            if (!rule) {
                throwRuleNotFoundError(parseRuleId(ruleId), config);
            }

            // Precompile and cache validator the first time
            if (!this.validators.has(rule)) {
                const schema = getRuleOptionsSchema(rule);

                if (schema) {
                    this.validators.set(rule, ajv.compile(schema));
                }
            }

            const validateRule = this.validators.get(rule);

            if (validateRule) {

                validateRule(ruleOptions.slice(1));

                if (validateRule.errors) {
                    throw new Error(`Key "rules": Key "${ruleId}": ${
                        validateRule.errors.map(
                            error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`
                        ).join("")
                    }`);
                }
            }
        }
    }
}

exports.RuleValidator = RuleValidator;


code:
/**
 * @fileoverview Helper functions for ESLint class
 * @author Nicholas C. Zakas
 */

"use strict";

//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------

const path = require("path");
const fs = require("fs");
const fsp = fs.promises;
const isGlob = require("is-glob");
const hash = require("../cli-engine/hash");
const minimatch = require("minimatch");
const util = require("util");
const fswalk = require("@nodelib/fs.walk");
const globParent = require("glob-parent");
const isPathInside = require("is-path-inside");

//-----------------------------------------------------------------------------
// Fixup references
//-----------------------------------------------------------------------------

const doFsWalk = util.promisify(fswalk.walk);
const Minimatch = minimatch.Minimatch;
const MINIMATCH_OPTIONS = { dot: true };

//-----------------------------------------------------------------------------
// Types
//-----------------------------------------------------------------------------

/**
 * @typedef {Object} GlobSearch
 * @property {Array<string>} patterns The normalized patterns to use for a search.
 * @property {Array<string>} rawPatterns The patterns as entered by the user
 *      before doing any normalization.
 */

//-----------------------------------------------------------------------------
// Errors
//-----------------------------------------------------------------------------

/**
 * 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} globEnabled If `false` then the pattern was a glob pattern, but glob was disabled.
     */
    constructor(pattern, globEnabled) {
        super(`No files matching '${pattern}' were found${!globEnabled ? " (glob was disabled)" : ""}.`);
        this.messageTemplate = "file-not-found";
        this.messageData = { pattern, globDisabled: !globEnabled };
    }
}

/**
 * The error type when a search fails to match multiple patterns.
 */
class UnmatchedSearchPatternsError extends Error {

    /**
     * @param {Object} options The options for the error.
     * @param {string} options.basePath The directory that was searched.
     * @param {Array<string>} options.unmatchedPatterns The glob patterns
     *      which were not found.
     * @param {Array<string>} options.patterns The glob patterns that were
     *      searched.
     * @param {Array<string>} options.rawPatterns The raw glob patterns that
     *      were searched.
     */
    constructor({ basePath, unmatchedPatterns, patterns, rawPatterns }) {
        super(`No files matching '${rawPatterns}' in '${basePath}' were found.`);
        this.basePath = basePath;
        this.unmatchedPatterns = unmatchedPatterns;
        this.patterns = patterns;
        this.rawPatterns = rawPatterns;
    }
}

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

//-----------------------------------------------------------------------------
// General Helpers
//-----------------------------------------------------------------------------

/**
 * Check if a given value is a non-empty string or not.
 * @param {any} x The value to check.
 * @returns {boolean} `true` if `x` is a non-empty string.
 */
function isNonEmptyString(x) {
    return typeof x === "string" && x.trim() !== "";
}

/**
 * Check if a given value is an array of non-empty strings or not.
 * @param {any} x The value to check.
 * @returns {boolean} `true` if `x` is an array of non-empty strings.
 */
function isArrayOfNonEmptyString(x) {
    return Array.isArray(x) && x.every(isNonEmptyString);
}

//-----------------------------------------------------------------------------
// File-related Helpers
//-----------------------------------------------------------------------------

/**
 * Normalizes slashes in a file pattern to posix-style.
 * @param {string} pattern The pattern to replace slashes in.
 * @returns {string} The pattern with slashes normalized.
 */
function normalizeToPosix(pattern) {
    return pattern.replace(/\\/gu, "/");
}

/**
 * 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 === "\\" ? normalizeToPosix(pattern) : pattern);
}

/**
 * Determines if a given glob pattern will return any results.
 * Used primarily to help with useful error messages.
 * @param {Object} options The options for the function.
 * @param {string} options.basePath The directory to search.
 * @param {string} options.pattern A glob pattern to match.
 * @returns {Promise<boolean>} True if there is a glob match, false if not.
 */
function globMatch({ basePath, pattern }) {

    let found = false;
    const patternToUse = path.isAbsolute(pattern)
        ? normalizeToPosix(path.relative(basePath, pattern))
        : pattern;

    const matcher = new Minimatch(patternToUse, MINIMATCH_OPTIONS);

    const fsWalkSettings = {

        deepFilter(entry) {
            const relativePath = normalizeToPosix(path.relative(basePath, entry.path));

            return !found && matcher.match(relativePath, true);
        },

        entryFilter(entry) {
            if (found || entry.dirent.isDirectory()) {
                return false;
            }

            const relativePath = normalizeToPosix(path.relative(basePath, entry.path));

            if (matcher.match(relativePath)) {
                found = true;
                return true;
            }

            return false;
        }
    };

    return new Promise(resolve => {

        // using a stream so we can exit early because we just need one match
        const globStream = fswalk.walkStream(basePath, fsWalkSettings);

        globStream.on("data", () => {
            globStream.destroy();
            resolve(true);
        });

        // swallow errors as they're not important here
        globStream.on("error", () => { });

        globStream.on("end", () => {
            resolve(false);
        });
        globStream.read();
    });

}

/**
 * Searches a directory looking for matching glob patterns. This uses
 * the config array's logic to determine if a directory or file should
 * be ignored, so it is consistent with how ignoring works throughout
 * ESLint.
 * @param {Object} options The options for this function.
 * @param {string} options.basePath The directory to search.
 * @param {Array<string>} options.patterns An array of glob patterns
 *      to match.
 * @param {Array<string>} options.rawPatterns An array of glob patterns
 *      as the user inputted them. Used for errors.
 * @param {FlatConfigArray} options.configs The config array to use for
 *      determining what to ignore.
 * @param {boolean} options.errorOnUnmatchedPattern Determines if an error
 *      should be thrown when a pattern is unmatched.
 * @returns {Promise<Array<string>>} An array of matching file paths
 *      or an empty array if there are no matches.
 * @throws {UnmatchedSearchPatternsError} If there is a pattern that doesn't
 *      match any files.
 */
async function globSearch({
    basePath,
    patterns,
    rawPatterns,
    configs,
    errorOnUnmatchedPattern
}) {

    if (patterns.length === 0) {
        return [];
    }

    /*
     * In this section we are converting the patterns into Minimatch
     * instances for performance reasons. Because we are doing the same
     * matches repeatedly, it's best to compile those patterns once and
     * reuse them multiple times.
     *
     * To do that, we convert any patterns with an absolute path into a
     * relative path and normalize it to Posix-style slashes. We also keep
     * track of the relative patterns to map them back to the original
     * patterns, which we need in order to throw an error if there are any
     * unmatched patterns.
     */
    const relativeToPatterns = new Map();
    const matchers = patterns.map((pattern, i) => {
        const patternToUse = path.isAbsolute(pattern)
            ? normalizeToPosix(path.relative(basePath, pattern))
            : pattern;

        relativeToPatterns.set(patternToUse, patterns[i]);

        return new Minimatch(patternToUse, MINIMATCH_OPTIONS);
    });

    /*
     * We track unmatched patterns because we may want to throw an error when
     * they occur. To start, this set is initialized with all of the patterns.
     * Every time a match occurs, the pattern is removed from the set, making
     * it easy to tell if we have any unmatched patterns left at the end of
     * search.
     */
    const unmatchedPatterns = new Set([...relativeToPatterns.keys()]);

    const filePaths = (await doFsWalk(basePath, {

        deepFilter(entry) {
            const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
            const matchesPattern = matchers.some(matcher => matcher.match(relativePath, true));

            return matchesPattern && !configs.isDirectoryIgnored(entry.path);
        },
        entryFilter(entry) {
            const relativePath = normalizeToPosix(path.relative(basePath, entry.path));

            // entries may be directories or files so filter out directories
            if (entry.dirent.isDirectory()) {
                return false;
            }

            /*
             * Optimization: We need to track when patterns are left unmatched
             * and so we use `unmatchedPatterns` to do that. There is a bit of
             * complexity here because the same file can be matched by more than
             * one pattern. So, when we start, we actually need to test every
             * pattern against every file. Once we know there are no remaining
             * unmatched patterns, then we can switch to just looking for the
             * first matching pattern for improved speed.
             */
            const matchesPattern = unmatchedPatterns.size > 0
                ? matchers.reduce((previousValue, matcher) => {
                    const pathMatches = matcher.match(relativePath);

                    /*
                     * We updated the unmatched patterns set only if the path
                     * matches and the file isn't ignored. If the file is
                     * ignored, that means there wasn't a match for the
                     * pattern so it should not be removed.
                     *
                     * Performance note: isFileIgnored() aggressively caches
                     * results so there is no performance penalty for calling
                     * it twice with the same argument.
                     */
                    if (pathMatches && !configs.isFileIgnored(entry.path)) {
                        unmatchedPatterns.delete(matcher.pattern);
                    }

                    return pathMatches || previousValue;
                }, false)
                : matchers.some(matcher => matcher.match(relativePath));

            return matchesPattern && !configs.isFileIgnored(entry.path);
        }

    })).map(entry => entry.path);

    // now check to see if we have any unmatched patterns
    if (errorOnUnmatchedPattern && unmatchedPatterns.size > 0) {
        throw new UnmatchedSearchPatternsError({
            basePath,
            unmatchedPatterns: [...unmatchedPatterns].map(
                pattern => relativeToPatterns.get(pattern)
            ),
            patterns,
            rawPatterns
        });
    }

    return filePaths;
}

/**
 * Throws an error for unmatched patterns. The error will only contain information about the first one.
 * Checks to see if there are any ignored results for a given search.
 * @param {Object} options The options for this function.
 * @param {string} options.basePath The directory to search.
 * @param {Array<string>} options.patterns An array of glob patterns
 *      that were used in the original search.
 * @param {Array<string>} options.rawPatterns An array of glob patterns
 *      as the user inputted them. Used for errors.
 * @param {Array<string>} options.unmatchedPatterns A non-empty array of glob patterns
 *      that were unmatched in the original search.
 * @returns {void} Always throws an error.
 * @throws {NoFilesFoundError} If the first unmatched pattern
 *      doesn't match any files even when there are no ignores.
 * @throws {AllFilesIgnoredError} If the first unmatched pattern
 *      matches some files when there are no ignores.
 */
async function throwErrorForUnmatchedPatterns({
    basePath,
    patterns,
    rawPatterns,
    unmatchedPatterns
}) {

    const pattern = unmatchedPatterns[0];
    const rawPattern = rawPatterns[patterns.indexOf(pattern)];

    const patternHasMatch = await globMatch({
        basePath,
        pattern
    });

    if (patternHasMatch) {
        throw new AllFilesIgnoredError(rawPattern);
    }

    // if we get here there are truly no matches
    throw new NoFilesFoundError(rawPattern, true);
}

/**
 * Performs multiple glob searches in parallel.
 * @param {Object} options The options for this function.
 * @param {Map<string,GlobSearch>} options.searches
 *      An array of glob patterns to match.
 * @param {FlatConfigArray} options.configs The config array to use for
 *      determining what to ignore.
 * @param {boolean} options.errorOnUnmatchedPattern Determines if an
 *      unmatched glob pattern should throw an error.
 * @returns {Promise<Array<string>>} An array of matching file paths
 *      or an empty array if there are no matches.
 */
async function globMultiSearch({ searches, configs, errorOnUnmatchedPattern }) {

    /*
     * For convenience, we normalized the search map into an array of objects.
     * Next, we filter out all searches that have no patterns. This happens
     * primarily for the cwd, which is prepopulated in the searches map as an
     * optimization. However, if it has no patterns, it means all patterns
     * occur outside of the cwd and we can safely filter out that search.
     */
    const normalizedSearches = [...searches].map(
        ([basePath, { patterns, rawPatterns }]) => ({ basePath, patterns, rawPatterns })
    ).filter(({ patterns }) => patterns.length > 0);

    const results = await Promise.allSettled(
        normalizedSearches.map(
            ({ basePath, patterns, rawPatterns }) => globSearch({
                basePath,
                patterns,
                rawPatterns,
                configs,
                errorOnUnmatchedPattern
            })
        )
    );

    const filePaths = [];

    for (let i = 0; i < results.length; i++) {

        const result = results[i];
        const currentSearch = normalizedSearches[i];

        if (result.status === "fulfilled") {

            // if the search was successful just add the results
            if (result.value.length > 0) {
                filePaths.push(...result.value);
            }

            continue;
        }

        // if we make it here then there was an error
        const error = result.reason;

        // unexpected errors should be re-thrown
        if (!error.basePath) {
            throw error;
        }

        if (errorOnUnmatchedPattern) {

            await throwErrorForUnmatchedPatterns({
                ...currentSearch,
                unmatchedPatterns: error.unmatchedPatterns
            });

        }

    }

    return [...new Set(filePaths)];

}

/**
 * Finds all files matching the options specified.
 * @param {Object} args The arguments objects.
 * @param {Array<string>} args.patterns An array of glob patterns.
 * @param {boolean} args.globInputPaths true to interpret glob patterns,
 *      false to not interpret glob patterns.
 * @param {string} args.cwd The current working directory to find from.
 * @param {FlatConfigArray} args.configs The configs for the current run.
 * @param {boolean} args.errorOnUnmatchedPattern Determines if an unmatched pattern
 *      should throw an error.
 * @returns {Promise<Array<string>>} The fully resolved file paths.
 * @throws {AllFilesIgnoredError} If there are no results due to an ignore pattern.
 * @throws {NoFilesFoundError} If no files matched the given patterns.
 */
async function findFiles({
    patterns,
    globInputPaths,
    cwd,
    configs,
    errorOnUnmatchedPattern
}) {

    const results = [];
    const missingPatterns = [];
    let globbyPatterns = [];
    let rawPatterns = [];
    const searches = new Map([[cwd, { patterns: globbyPatterns, rawPatterns: [] }]]);

    // check to see if we have explicit files and directories
    const filePaths = patterns.map(filePath => path.resolve(cwd, filePath));
    const stats = await Promise.all(
        filePaths.map(
            filePath => fsp.stat(filePath).catch(() => { })
        )
    );

    stats.forEach((stat, index) => {

        const filePath = filePaths[index];
        const pattern = normalizeToPosix(patterns[index]);

        if (stat) {

            // files are added directly to the list
            if (stat.isFile()) {
                results.push({
                    filePath,
                    ignored: configs.isFileIgnored(filePath)
                });
            }

            // directories need extensions attached
            if (stat.isDirectory()) {

                // group everything in cwd together and split out others
                if (isPathInside(filePath, cwd)) {
                    ({ patterns: globbyPatterns, rawPatterns } = searches.get(cwd));
                } else {
                    if (!searches.has(filePath)) {
                        searches.set(filePath, { patterns: [], rawPatterns: [] });
                    }
                    ({ patterns: globbyPatterns, rawPatterns } = searches.get(filePath));
                }

                globbyPatterns.push(`${normalizeToPosix(filePath)}/**`);
                rawPatterns.push(pattern);
            }

            return;
        }

        // save patterns for later use based on whether globs are enabled
        if (globInputPaths && isGlobPattern(pattern)) {

            const basePath = path.resolve(cwd, globParent(pattern));

            // group in cwd if possible and split out others
            if (isPathInside(basePath, cwd)) {
                ({ patterns: globbyPatterns, rawPatterns } = searches.get(cwd));
            } else {
                if (!searches.has(basePath)) {
                    searches.set(basePath, { patterns: [], rawPatterns: [] });
                }
                ({ patterns: globbyPatterns, rawPatterns } = searches.get(basePath));
            }

            globbyPatterns.push(filePath);
            rawPatterns.push(pattern);
        } else {
            missingPatterns.push(pattern);
        }
    });

    // there were patterns that didn't match anything, tell the user
    if (errorOnUnmatchedPattern && missingPatterns.length) {
        throw new NoFilesFoundError(missingPatterns[0], globInputPaths);
    }

    // now we are safe to do the search
    const globbyResults = await globMultiSearch({
        searches,
        configs,
        errorOnUnmatchedPattern
    });

    return [
        ...results,
        ...globbyResults.map(filePath => ({
            filePath: path.resolve(filePath),
            ignored: false
        }))
    ];
}

//-----------------------------------------------------------------------------
// Results-related Helpers
//-----------------------------------------------------------------------------

/**
 * Checks if the given message is an error message.
 * @param {LintMessage} message The message to check.
 * @returns {boolean} Whether or not the message is an error message.
 * @private
 */
function isErrorMessage(message) {
    return message.severity === 2;
}

/**
 * Returns result with warning by ignore settings
 * @param {string} filePath File path of checked code
 * @param {string} baseDir Absolute path of base directory
 * @returns {LintResult} Result with single warning
 * @private
 */
function createIgnoreResult(filePath, baseDir) {
    let message;
    const isInNodeModules = baseDir && path.dirname(path.relative(baseDir, filePath)).split(path.sep).includes("node_modules");

    if (isInNodeModules) {
        message = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning.";
    } else {
        message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning.";
    }

    return {
        filePath: path.resolve(filePath),
        messages: [
            {
                ruleId: null,
                fatal: false,
                severity: 1,
                message,
                nodeType: null
            }
        ],
        suppressedMessages: [],
        errorCount: 0,
        warningCount: 1,
        fatalErrorCount: 0,
        fixableErrorCount: 0,
        fixableWarningCount: 0
    };
}

//-----------------------------------------------------------------------------
// Options-related Helpers
//-----------------------------------------------------------------------------

/**
 * Check if a given value is a valid fix type or not.
 * @param {any} x The value to check.
 * @returns {boolean} `true` if `x` is valid fix type.
 */
function isFixType(x) {
    return x === "directive" || x === "problem" || x === "suggestion" || x === "layout";
}

/**
 * Check if a given value is an array of fix types or not.
 * @param {any} x The value to check.
 * @returns {boolean} `true` if `x` is an array of fix types.
 */
function isFixTypeArray(x) {
    return Array.isArray(x) && x.every(isFixType);
}

/**
 * The error for invalid options.
 */
class ESLintInvalidOptionsError extends Error {
    constructor(messages) {
        super(`Invalid Options:\n- ${messages.join("\n- ")}`);
        this.code = "ESLINT_INVALID_OPTIONS";
        Error.captureStackTrace(this, ESLintInvalidOptionsError);
    }
}

/**
 * Validates and normalizes options for the wrapped CLIEngine instance.
 * @param {FlatESLintOptions} options The options to process.
 * @throws {ESLintInvalidOptionsError} If of any of a variety of type errors.
 * @returns {FlatESLintOptions} The normalized options.
 */
function processOptions({
    allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instead because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored.
    baseConfig = null,
    cache = false,
    cacheLocation = ".eslintcache",
    cacheStrategy = "metadata",
    cwd = process.cwd(),
    errorOnUnmatchedPattern = true,
    fix = false,
    fixTypes = null, // ← should be null by default because if it's an array then it suppresses rules that don't have the `meta.type` property.
    globInputPaths = true,
    ignore = true,
    ignorePatterns = null,
    overrideConfig = null,
    overrideConfigFile = null,
    plugins = {},
    warnIgnored = true,
    ...unknownOptions
}) {
    const errors = [];
    const unknownOptionKeys = Object.keys(unknownOptions);

    if (unknownOptionKeys.length >= 1) {
        errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`);
        if (unknownOptionKeys.includes("cacheFile")) {
            errors.push("'cacheFile' has been removed. Please use the 'cacheLocation' option instead.");
        }
        if (unknownOptionKeys.includes("configFile")) {
            errors.push("'configFile' has been removed. Please use the 'overrideConfigFile' option instead.");
        }
        if (unknownOptionKeys.includes("envs")) {
            errors.push("'envs' has been removed.");
        }
        if (unknownOptionKeys.includes("extensions")) {
            errors.push("'extensions' has been removed.");
        }
        if (unknownOptionKeys.includes("resolvePluginsRelativeTo")) {
            errors.push("'resolvePluginsRelativeTo' has been removed.");
        }
        if (unknownOptionKeys.includes("globals")) {
            errors.push("'globals' has been removed. Please use the 'overrideConfig.languageOptions.globals' option instead.");
        }
        if (unknownOptionKeys.includes("ignorePath")) {
            errors.push("'ignorePath' has been removed.");
        }
        if (unknownOptionKeys.includes("ignorePattern")) {
            errors.push("'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead.");
        }
        if (unknownOptionKeys.includes("parser")) {
            errors.push("'parser' has been removed. Please use the 'overrideConfig.languageOptions.parser' option instead.");
        }
        if (unknownOptionKeys.includes("parserOptions")) {
            errors.push("'parserOptions' has been removed. Please use the 'overrideConfig.languageOptions.parserOptions' option instead.");
        }
        if (unknownOptionKeys.includes("rules")) {
            errors.push("'rules' has been removed. Please use the 'overrideConfig.rules' option instead.");
        }
        if (unknownOptionKeys.includes("rulePaths")) {
            errors.push("'rulePaths' has been removed. Please define your rules using plugins.");
        }
        if (unknownOptionKeys.includes("reportUnusedDisableDirectives")) {
            errors.push("'reportUnusedDisableDirectives' has been removed. Please use the 'overrideConfig.linterOptions.reportUnusedDisableDirectives' option instead.");
        }
    }
    if (typeof allowInlineConfig !== "boolean") {
        errors.push("'allowInlineConfig' must be a boolean.");
    }
    if (typeof baseConfig !== "object") {
        errors.push("'baseConfig' must be an object or null.");
    }
    if (typeof cache !== "boolean") {
        errors.push("'cache' must be a boolean.");
    }
    if (!isNonEmptyString(cacheLocation)) {
        errors.push("'cacheLocation' must be a non-empty string.");
    }
    if (
        cacheStrategy !== "metadata" &&
        cacheStrategy !== "content"
    ) {
        errors.push("'cacheStrategy' must be any of \"metadata\", \"content\".");
    }
    if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) {
        errors.push("'cwd' must be an absolute path.");
    }
    if (typeof errorOnUnmatchedPattern !== "boolean") {
        errors.push("'errorOnUnmatchedPattern' must be a boolean.");
    }
    if (typeof fix !== "boolean" && typeof fix !== "function") {
        errors.push("'fix' must be a boolean or a function.");
    }
    if (fixTypes !== null && !isFixTypeArray(fixTypes)) {
        errors.push("'fixTypes' must be an array of any of \"directive\", \"problem\", \"suggestion\", and \"layout\".");
    }
    if (typeof globInputPaths !== "boolean") {
        errors.push("'globInputPaths' must be a boolean.");
    }
    if (typeof ignore !== "boolean") {
        errors.push("'ignore' must be a boolean.");
    }
    if (!isArrayOfNonEmptyString(ignorePatterns) && ignorePatterns !== null) {
        errors.push("'ignorePatterns' must be an array of non-empty strings or null.");
    }
    if (typeof overrideConfig !== "object") {
        errors.push("'overrideConfig' must be an object or null.");
    }
    if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null && overrideConfigFile !== true) {
        errors.push("'overrideConfigFile' must be a non-empty string, null, or true.");
    }
    if (typeof plugins !== "object") {
        errors.push("'plugins' must be an object or null.");
    } else if (plugins !== null && Object.keys(plugins).includes("")) {
        errors.push("'plugins' must not include an empty string.");
    }
    if (Array.isArray(plugins)) {
        errors.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead.");
    }
    if (typeof warnIgnored !== "boolean") {
        errors.push("'warnIgnored' must be a boolean.");
    }
    if (errors.length > 0) {
        throw new ESLintInvalidOptionsError(errors);
    }

    return {
        allowInlineConfig,
        baseConfig,
        cache,
        cacheLocation,
        cacheStrategy,

        // when overrideConfigFile is true that means don't do config file lookup
        configFile: overrideConfigFile === true ? false : overrideConfigFile,
        overrideConfig,
        cwd: path.normalize(cwd),
        errorOnUnmatchedPattern,
        fix,
        fixTypes,
        globInputPaths,
        ignore,
        ignorePatterns,
        warnIgnored
    };
}

//-----------------------------------------------------------------------------
// Cache-related helpers
//-----------------------------------------------------------------------------

/**
 * return the cacheFile to be used by eslint, based on whether the provided parameter is
 * a directory or looks like a directory (ends in `path.sep`), in which case the file
 * name will be the `cacheFile/.cache_hashOfCWD`
 *
 * if cacheFile points to a file or looks like a file then in will just use that file
 * @param {string} cacheFile The name of file to be used to store the cache
 * @param {string} cwd Current working directory
 * @returns {string} the resolved path to the cache file
 */
function getCacheFile(cacheFile, cwd) {

    /*
     * make sure the path separators are normalized for the environment/os
     * keeping the trailing path separator if present
     */
    const normalizedCacheFile = path.normalize(cacheFile);

    const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile);
    const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep;

    /**
     * return the name for the cache file in case the provided parameter is a directory
     * @returns {string} the resolved path to the cacheFile
     */
    function getCacheFileForDirectory() {
        return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
    }

    let fileStats;

    try {
        fileStats = fs.lstatSync(resolvedCacheFile);
    } catch {
        fileStats = null;
    }

    /*
     * in case the file exists we need to verify if the provided path
     * is a directory or a file. If it is a directory we want to create a file
     * inside that directory
     */
    if (fileStats) {

        /*
         * is a directory or is a file, but the original file the user provided
         * looks like a directory but `path.resolve` removed the `last path.sep`
         * so we need to still treat this like a directory
         */
        if (fileStats.isDirectory() || looksLikeADirectory) {
            return getCacheFileForDirectory();
        }

        // is file so just use that file
        return resolvedCacheFile;
    }

    /*
     * here we known the file or directory doesn't exist,
     * so we will try to infer if its a directory if it looks like a directory
     * for the current operating system.
     */

    // if the last character passed is a path separator we assume is a directory
    if (looksLikeADirectory) {
        return getCacheFileForDirectory();
    }

    return resolvedCacheFile;
}

//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------

module.exports = {
    isGlobPattern,
    findFiles,

    isNonEmptyString,
    isArrayOfNonEmptyString,

    createIgnoreResult,
    isErrorMessage,

    processOptions,

    getCacheFile
};