472 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			472 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @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;
 |