123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- /**
- * @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 warningCount = 0;
- for (const result of results) {
- errorCount += result.errorCount;
- warningCount += result.warningCount;
- }
- return { errorCount, 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 = 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, warningCount } = countErrors(results);
- const tooManyWarnings =
- options.maxWarnings >= 0 && warningCount > options.maxWarnings;
- if (!errorCount && tooManyWarnings) {
- log.error(
- "ESLint found too many warnings (maximum: %s).",
- options.maxWarnings
- );
- }
- return (errorCount || tooManyWarnings) ? 1 : 0;
- }
- return 2;
- }
- };
- module.exports = cli;
|