| /** |
| * @fileoverview Main CLI object. |
| * @author Nicholas C. Zakas |
| */ |
| |
| "use strict"; |
| |
| /* |
| * 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"), |
| CLIOptions = require("./options"), |
| log = require("./shared/logging"), |
| RuntimeInfo = require("./shared/runtime-info"); |
| |
| 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 */ |
| |
| //------------------------------------------------------------------------------ |
| // 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 CLIEngine. |
| * @param {ParsedCLIOptions} cliOptions The CLI options to translate. |
| * @returns {ESLintOptions} The options object for the CLIEngine. |
| * @private |
| */ |
| function translateOptions({ |
| cache, |
| cacheFile, |
| cacheLocation, |
| cacheStrategy, |
| config, |
| env, |
| errorOnUnmatchedPattern, |
| eslintrc, |
| ext, |
| fix, |
| fixDryRun, |
| fixType, |
| global, |
| ignore, |
| ignorePath, |
| ignorePattern, |
| inlineConfig, |
| parser, |
| parserOptions, |
| plugin, |
| quiet, |
| reportUnusedDisableDirectives, |
| resolvePluginsRelativeTo, |
| rule, |
| rulesdir |
| }) { |
| return { |
| allowInlineConfig: inlineConfig, |
| cache, |
| cacheLocation: cacheLocation || cacheFile, |
| cacheStrategy, |
| errorOnUnmatchedPattern, |
| extensions: ext, |
| fix: (fix || fixDryRun) && (quiet ? quietFixPredicate : true), |
| fixTypes: fixType, |
| ignore, |
| ignorePath, |
| 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 |
| }, |
| overrideConfigFile: config, |
| reportUnusedDisableDirectives: reportUnusedDisableDirectives ? "error" : void 0, |
| resolvePluginsRelativeTo, |
| rulePaths: rulesdir, |
| useEslintrc: eslintrc |
| }; |
| } |
| |
| /** |
| * Count error messages. |
| * @param {LintResult[]} results The lint results. |
| * @returns {{errorCount: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. |
| * @returns {Promise<boolean>} True if the printing succeeds, false if not. |
| * @private |
| */ |
| async function printResults(engine, results, format, outputFile) { |
| let formatter; |
| |
| try { |
| formatter = await engine.loadFormatter(format); |
| } catch (e) { |
| log.error(e.message); |
| return false; |
| } |
| |
| const output = await formatter.format(results); |
| |
| 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). |
| * @returns {Promise<number>} The exit code for the operation. |
| */ |
| async execute(args, text) { |
| if (Array.isArray(args)) { |
| debug("CLI args: %o", args.slice(2)); |
| } |
| |
| /** @type {ParsedCLIOptions} */ |
| let options; |
| |
| try { |
| options = CLIOptions.parse(args); |
| } catch (error) { |
| 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) { |
| 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 = new ESLint(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 engine = new ESLint(translateOptions(options)); |
| 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 ESLint.outputFixes(results); |
| } |
| |
| let resultsToPrint = results; |
| |
| if (options.quiet) { |
| debug("Quiet mode enabled - filtering out warnings"); |
| resultsToPrint = ESLint.getErrorResults(resultsToPrint); |
| } |
| |
| if (await printResults(engine, resultsToPrint, options.format, options.outputFile)) { |
| |
| // Errors and warnings from the original unfiltered results should determine the exit code |
| const { errorCount, fatalErrorCount, warningCount } = countErrors(results); |
| |
| const tooManyWarnings = |
| options.maxWarnings >= 0 && warningCount > options.maxWarnings; |
| const shouldExitForFatalErrors = |
| options.exitOnFatalError && fatalErrorCount > 0; |
| |
| if (!errorCount && tooManyWarnings) { |
| log.error( |
| "ESLint found too many warnings (maximum: %s).", |
| options.maxWarnings |
| ); |
| } |
| |
| if (shouldExitForFatalErrors) { |
| return 2; |
| } |
| |
| return (errorCount || tooManyWarnings) ? 1 : 0; |
| } |
| |
| return 2; |
| } |
| }; |
| |
| module.exports = cli; |