| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 |
- /**
- * @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("node:fs"),
- { mkdir, stat, writeFile } = require("node:fs/promises"),
- path = require("node:path"),
- { pathToFileURL } = require("node:url"),
- { LegacyESLint } = require("./eslint"),
- {
- ESLint,
- shouldUseFlatConfig,
- locateConfigFileToUse,
- } = require("./eslint/eslint"),
- createCLIOptions = require("./options"),
- log = require("./shared/logging"),
- RuntimeInfo = require("./shared/runtime-info"),
- translateOptions = require("./shared/translate-cli-options");
- const { getCacheFile } = require("./eslint/eslint-helpers");
- const { SuppressionsService } = require("./services/suppressions-service");
- const debug = require("debug")("eslint:cli");
- //------------------------------------------------------------------------------
- // Types
- //------------------------------------------------------------------------------
- /** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
- /** @typedef {import("./types").ESLint.LintResult} LintResult */
- /** @typedef {import("./types").ESLint.ResultsMeta} ResultsMeta */
- //------------------------------------------------------------------------------
- // Helpers
- //------------------------------------------------------------------------------
- /**
- * 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 };
- }
- /**
- * Creates an options module from the provided CLI options and encodes it as a data URL.
- * @param {ParsedCLIOptions} options The CLI options.
- * @returns {URL} The URL of the options module.
- */
- function createOptionsModule(options) {
- const translateOptionsFileURL = new URL(
- "./shared/translate-cli-options.js",
- pathToFileURL(__filename),
- ).href;
- const optionsSrc =
- `import translateOptions from ${JSON.stringify(translateOptionsFileURL)};\n` +
- `export default await translateOptions(${JSON.stringify(options)}, "flat");\n`;
- // Base64 encoding is typically shorter than URL encoding
- return new URL(
- `data:text/javascript;base64,${Buffer.from(optionsSrc).toString("base64")}`,
- );
- }
- /**
- * 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 (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 if (output) {
- log.info(output);
- }
- return true;
- }
- /**
- * Validates the `--concurrency` flag value.
- * @param {string} concurrency The `--concurrency` flag value to validate.
- * @returns {void}
- * @throws {Error} If the `--concurrency` flag value is invalid.
- */
- function validateConcurrency(concurrency) {
- if (
- concurrency === void 0 ||
- concurrency === "auto" ||
- concurrency === "off"
- ) {
- return;
- }
- const concurrencyValue = Number(concurrency);
- if (!Number.isInteger(concurrencyValue) || concurrencyValue < 1) {
- throw new Error(
- `Option concurrency: '${concurrency}' is not a positive integer, 'auto' or 'off'.`,
- );
- }
- }
- //------------------------------------------------------------------------------
- // 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 = {
- /**
- * Calculates the command string for the --inspect-config operation.
- * @param {string} configFile The path to the config file to inspect.
- * @returns {Promise<string>} The command string to execute.
- */
- async calculateInspectConfigFlags(configFile) {
- // find the config file
- const { configFilePath, basePath } = await locateConfigFileToUse({
- cwd: process.cwd(),
- configFile,
- });
- return ["--config", configFilePath, "--basePath", basePath];
- },
- /**
- * 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=true] Whether or not to allow flat config.
- * @returns {Promise<number>} The exit code for the operation.
- */
- async execute(args, text, allowFlatConfig = true) {
- 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);
- if (allowFlatConfig && !usingFlatConfig) {
- const { WarningService } = require("./services/warning-service");
- new WarningService().emitESLintRCWarning();
- }
- const CLIOptions = createCLIOptions(usingFlatConfig);
- /** @type {ParsedCLIOptions} */
- let options;
- try {
- options = CLIOptions.parse(args);
- validateConcurrency(options.concurrency);
- } 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 ESLint(await translateOptions(options, "flat"))
- : new LegacyESLint(await translateOptions(options));
- const fileConfig = await engine.calculateConfigForFile(
- options.printConfig,
- );
- log.info(JSON.stringify(fileConfig, null, " "));
- return 0;
- }
- if (options.inspectConfig) {
- log.info(
- "You can also run this command directly using 'npx @eslint/config-inspector@latest' in the same directory as your configuration file.",
- );
- try {
- const flatOptions = await translateOptions(options, "flat");
- const spawn = require("cross-spawn");
- const flags = await cli.calculateInspectConfigFlags(
- flatOptions.overrideConfigFile,
- );
- spawn.sync(
- "npx",
- ["@eslint/config-inspector@latest", ...flags],
- { encoding: "utf8", stdio: "inherit" },
- );
- } catch (error) {
- log.error(error);
- return 2;
- }
- 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;
- }
- if (usingFlatConfig && options.ext) {
- // Passing `--ext ""` results in `options.ext` being an empty array.
- if (options.ext.length === 0) {
- log.error("The --ext option value cannot be empty.");
- return 2;
- }
- // Passing `--ext ,ts` results in an empty string at index 0. Passing `--ext ts,,tsx` results in an empty string at index 1.
- const emptyStringIndex = options.ext.indexOf("");
- if (emptyStringIndex >= 0) {
- log.error(
- `The --ext option arguments cannot be empty strings. Found an empty string at index ${emptyStringIndex}.`,
- );
- return 2;
- }
- }
- if (options.suppressAll && options.suppressRule) {
- log.error(
- "The --suppress-all option and the --suppress-rule option cannot be used together.",
- );
- return 2;
- }
- if (options.suppressAll && options.pruneSuppressions) {
- log.error(
- "The --suppress-all option and the --prune-suppressions option cannot be used together.",
- );
- return 2;
- }
- if (options.suppressRule && options.pruneSuppressions) {
- log.error(
- "The --suppress-rule option and the --prune-suppressions option cannot be used together.",
- );
- return 2;
- }
- if (
- useStdin &&
- (options.suppressAll ||
- options.suppressRule ||
- options.pruneSuppressions)
- ) {
- log.error(
- "The --suppress-all, --suppress-rule, and --prune-suppressions options cannot be used with piped-in code.",
- );
- return 2;
- }
- const ActiveESLint = usingFlatConfig ? ESLint : LegacyESLint;
- /** @type {ESLint|LegacyESLint} */
- let engine;
- if (options.concurrency && options.concurrency !== "off") {
- const optionsURL = createOptionsModule(options);
- engine = await ESLint.fromOptionsModule(optionsURL);
- } else {
- const eslintOptions = await translateOptions(
- options,
- usingFlatConfig ? "flat" : "eslintrc",
- );
- engine = new ActiveESLint(eslintOptions);
- }
- 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 unusedSuppressions = {};
- if (!useStdin) {
- const suppressionsFileLocation = getCacheFile(
- options.suppressionsLocation || "eslint-suppressions.json",
- process.cwd(),
- {
- prefix: "suppressions_",
- },
- );
- if (
- options.suppressionsLocation &&
- !fs.existsSync(suppressionsFileLocation) &&
- !options.suppressAll &&
- !options.suppressRule
- ) {
- log.error(
- "The suppressions file does not exist. Please run the command with `--suppress-all` or `--suppress-rule` to create it.",
- );
- return 2;
- }
- if (
- options.suppressAll ||
- options.suppressRule ||
- options.pruneSuppressions ||
- fs.existsSync(suppressionsFileLocation)
- ) {
- const suppressions = new SuppressionsService({
- filePath: suppressionsFileLocation,
- cwd: process.cwd(),
- });
- if (options.suppressAll || options.suppressRule) {
- await suppressions.suppress(results, options.suppressRule);
- }
- if (options.pruneSuppressions) {
- await suppressions.prune(results);
- }
- const suppressionResults = suppressions.applySuppressions(
- results,
- await suppressions.load(),
- );
- results = suppressionResults.results;
- unusedSuppressions = suppressionResults.unused;
- }
- }
- 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 (!options.passOnUnprunedSuppressions) {
- const unusedSuppressionsCount =
- Object.keys(unusedSuppressions).length;
- if (unusedSuppressionsCount > 0) {
- log.error(
- "There are suppressions left that do not occur anymore. Consider re-running the command with `--prune-suppressions`.",
- );
- debug(JSON.stringify(unusedSuppressions, null, 2));
- return 2;
- }
- }
- if (shouldExitForFatalErrors) {
- return 2;
- }
- return resultCounts.errorCount || tooManyWarnings ? 1 : 0;
- }
- return 2;
- },
- };
- module.exports = cli;
|