| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 | /** * @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 } = require("./eslint/flat-eslint"),    createCLIOptions = require("./options"),    log = require("./shared/logging"),    RuntimeInfo = require("./shared/runtime-info");const { Legacy: { naming } } = require("@eslint/eslintrc");const { findFlatConfigFile } = require("./eslint/flat-eslint");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,    resolvePluginsRelativeTo,    rule,    rulesdir}, 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 (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,        reportUnusedDisableDirectives: reportUnusedDisableDirectives ? "error" : void 0    };    if (configType === "flat") {        options.ignorePatterns = ignorePattern;    } else {        options.resolvePluginsRelativeTo = resolvePluginsRelativeTo;        options.rulePaths = rulesdir;        options.useEslintrc = eslintrc;        options.extensions = ext;        options.ignorePath = ignorePath;    }    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;}/** * Returns whether flat config should be used. * @param {boolean} [allowFlatConfig] Whether or not to allow flat config. * @returns {Promise<boolean>} Where flat config should be used. */async function shouldUseFlatConfig(allowFlatConfig) {    if (!allowFlatConfig) {        return false;    }    switch (process.env.ESLINT_USE_FLAT_CONFIG) {        case "true":            return true;        case "false":            return false;        default:            /*             * If neither explicitly enabled nor disabled, then use the presence             * of a flat config file to determine enablement.             */            return !!(await findFlatConfigFile(process.cwd()));    }}//------------------------------------------------------------------------------// 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 = await shouldUseFlatConfig(allowFlatConfig);        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);            log.error(error.message);            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;        }        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,                warnIgnored: 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;
 |