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