cli.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. /**
  2. * @fileoverview Main CLI object.
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. /*
  7. * NOTE: The CLI object should *not* call process.exit() directly. It should only return
  8. * exit codes. This allows other programs to use the CLI object and still control
  9. * when the program exits.
  10. */
  11. //------------------------------------------------------------------------------
  12. // Requirements
  13. //------------------------------------------------------------------------------
  14. const fs = require("fs"),
  15. path = require("path"),
  16. { promisify } = require("util"),
  17. { ESLint } = require("./eslint"),
  18. { FlatESLint, shouldUseFlatConfig } = require("./eslint/flat-eslint"),
  19. createCLIOptions = require("./options"),
  20. log = require("./shared/logging"),
  21. RuntimeInfo = require("./shared/runtime-info"),
  22. { normalizeSeverityToString } = require("./shared/severity");
  23. const { Legacy: { naming } } = require("@eslint/eslintrc");
  24. const { ModuleImporter } = require("@humanwhocodes/module-importer");
  25. const debug = require("debug")("eslint:cli");
  26. //------------------------------------------------------------------------------
  27. // Types
  28. //------------------------------------------------------------------------------
  29. /** @typedef {import("./eslint/eslint").ESLintOptions} ESLintOptions */
  30. /** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
  31. /** @typedef {import("./eslint/eslint").LintResult} LintResult */
  32. /** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
  33. /** @typedef {import("./shared/types").ResultsMeta} ResultsMeta */
  34. //------------------------------------------------------------------------------
  35. // Helpers
  36. //------------------------------------------------------------------------------
  37. const mkdir = promisify(fs.mkdir);
  38. const stat = promisify(fs.stat);
  39. const writeFile = promisify(fs.writeFile);
  40. /**
  41. * Predicate function for whether or not to apply fixes in quiet mode.
  42. * If a message is a warning, do not apply a fix.
  43. * @param {LintMessage} message The lint result.
  44. * @returns {boolean} True if the lint message is an error (and thus should be
  45. * autofixed), false otherwise.
  46. */
  47. function quietFixPredicate(message) {
  48. return message.severity === 2;
  49. }
  50. /**
  51. * Translates the CLI options into the options expected by the ESLint constructor.
  52. * @param {ParsedCLIOptions} cliOptions The CLI options to translate.
  53. * @param {"flat"|"eslintrc"} [configType="eslintrc"] The format of the
  54. * config to generate.
  55. * @returns {Promise<ESLintOptions>} The options object for the ESLint constructor.
  56. * @private
  57. */
  58. async function translateOptions({
  59. cache,
  60. cacheFile,
  61. cacheLocation,
  62. cacheStrategy,
  63. config,
  64. configLookup,
  65. env,
  66. errorOnUnmatchedPattern,
  67. eslintrc,
  68. ext,
  69. fix,
  70. fixDryRun,
  71. fixType,
  72. global,
  73. ignore,
  74. ignorePath,
  75. ignorePattern,
  76. inlineConfig,
  77. parser,
  78. parserOptions,
  79. plugin,
  80. quiet,
  81. reportUnusedDisableDirectives,
  82. reportUnusedDisableDirectivesSeverity,
  83. resolvePluginsRelativeTo,
  84. rule,
  85. rulesdir,
  86. warnIgnored
  87. }, configType) {
  88. let overrideConfig, overrideConfigFile;
  89. const importer = new ModuleImporter();
  90. if (configType === "flat") {
  91. overrideConfigFile = (typeof config === "string") ? config : !configLookup;
  92. if (overrideConfigFile === false) {
  93. overrideConfigFile = void 0;
  94. }
  95. let globals = {};
  96. if (global) {
  97. globals = global.reduce((obj, name) => {
  98. if (name.endsWith(":true")) {
  99. obj[name.slice(0, -5)] = "writable";
  100. } else {
  101. obj[name] = "readonly";
  102. }
  103. return obj;
  104. }, globals);
  105. }
  106. overrideConfig = [{
  107. languageOptions: {
  108. globals,
  109. parserOptions: parserOptions || {}
  110. },
  111. rules: rule ? rule : {}
  112. }];
  113. if (reportUnusedDisableDirectives || reportUnusedDisableDirectivesSeverity !== void 0) {
  114. overrideConfig[0].linterOptions = {
  115. reportUnusedDisableDirectives: reportUnusedDisableDirectives
  116. ? "error"
  117. : normalizeSeverityToString(reportUnusedDisableDirectivesSeverity)
  118. };
  119. }
  120. if (parser) {
  121. overrideConfig[0].languageOptions.parser = await importer.import(parser);
  122. }
  123. if (plugin) {
  124. const plugins = {};
  125. for (const pluginName of plugin) {
  126. const shortName = naming.getShorthandName(pluginName, "eslint-plugin");
  127. const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
  128. plugins[shortName] = await importer.import(longName);
  129. }
  130. overrideConfig[0].plugins = plugins;
  131. }
  132. } else {
  133. overrideConfigFile = config;
  134. overrideConfig = {
  135. env: env && env.reduce((obj, name) => {
  136. obj[name] = true;
  137. return obj;
  138. }, {}),
  139. globals: global && global.reduce((obj, name) => {
  140. if (name.endsWith(":true")) {
  141. obj[name.slice(0, -5)] = "writable";
  142. } else {
  143. obj[name] = "readonly";
  144. }
  145. return obj;
  146. }, {}),
  147. ignorePatterns: ignorePattern,
  148. parser,
  149. parserOptions,
  150. plugins: plugin,
  151. rules: rule
  152. };
  153. }
  154. const options = {
  155. allowInlineConfig: inlineConfig,
  156. cache,
  157. cacheLocation: cacheLocation || cacheFile,
  158. cacheStrategy,
  159. errorOnUnmatchedPattern,
  160. fix: (fix || fixDryRun) && (quiet ? quietFixPredicate : true),
  161. fixTypes: fixType,
  162. ignore,
  163. overrideConfig,
  164. overrideConfigFile
  165. };
  166. if (configType === "flat") {
  167. options.ignorePatterns = ignorePattern;
  168. options.warnIgnored = warnIgnored;
  169. } else {
  170. options.resolvePluginsRelativeTo = resolvePluginsRelativeTo;
  171. options.rulePaths = rulesdir;
  172. options.useEslintrc = eslintrc;
  173. options.extensions = ext;
  174. options.ignorePath = ignorePath;
  175. if (reportUnusedDisableDirectives || reportUnusedDisableDirectivesSeverity !== void 0) {
  176. options.reportUnusedDisableDirectives = reportUnusedDisableDirectives
  177. ? "error"
  178. : normalizeSeverityToString(reportUnusedDisableDirectivesSeverity);
  179. }
  180. }
  181. return options;
  182. }
  183. /**
  184. * Count error messages.
  185. * @param {LintResult[]} results The lint results.
  186. * @returns {{errorCount:number;fatalErrorCount:number,warningCount:number}} The number of error messages.
  187. */
  188. function countErrors(results) {
  189. let errorCount = 0;
  190. let fatalErrorCount = 0;
  191. let warningCount = 0;
  192. for (const result of results) {
  193. errorCount += result.errorCount;
  194. fatalErrorCount += result.fatalErrorCount;
  195. warningCount += result.warningCount;
  196. }
  197. return { errorCount, fatalErrorCount, warningCount };
  198. }
  199. /**
  200. * Check if a given file path is a directory or not.
  201. * @param {string} filePath The path to a file to check.
  202. * @returns {Promise<boolean>} `true` if the given path is a directory.
  203. */
  204. async function isDirectory(filePath) {
  205. try {
  206. return (await stat(filePath)).isDirectory();
  207. } catch (error) {
  208. if (error.code === "ENOENT" || error.code === "ENOTDIR") {
  209. return false;
  210. }
  211. throw error;
  212. }
  213. }
  214. /**
  215. * Outputs the results of the linting.
  216. * @param {ESLint} engine The ESLint instance to use.
  217. * @param {LintResult[]} results The results to print.
  218. * @param {string} format The name of the formatter to use or the path to the formatter.
  219. * @param {string} outputFile The path for the output file.
  220. * @param {ResultsMeta} resultsMeta Warning count and max threshold.
  221. * @returns {Promise<boolean>} True if the printing succeeds, false if not.
  222. * @private
  223. */
  224. async function printResults(engine, results, format, outputFile, resultsMeta) {
  225. let formatter;
  226. try {
  227. formatter = await engine.loadFormatter(format);
  228. } catch (e) {
  229. log.error(e.message);
  230. return false;
  231. }
  232. const output = await formatter.format(results, resultsMeta);
  233. if (output) {
  234. if (outputFile) {
  235. const filePath = path.resolve(process.cwd(), outputFile);
  236. if (await isDirectory(filePath)) {
  237. log.error("Cannot write to output file path, it is a directory: %s", outputFile);
  238. return false;
  239. }
  240. try {
  241. await mkdir(path.dirname(filePath), { recursive: true });
  242. await writeFile(filePath, output);
  243. } catch (ex) {
  244. log.error("There was a problem writing the output file:\n%s", ex);
  245. return false;
  246. }
  247. } else {
  248. log.info(output);
  249. }
  250. }
  251. return true;
  252. }
  253. //------------------------------------------------------------------------------
  254. // Public Interface
  255. //------------------------------------------------------------------------------
  256. /**
  257. * Encapsulates all CLI behavior for eslint. Makes it easier to test as well as
  258. * for other Node.js programs to effectively run the CLI.
  259. */
  260. const cli = {
  261. /**
  262. * Executes the CLI based on an array of arguments that is passed in.
  263. * @param {string|Array|Object} args The arguments to process.
  264. * @param {string} [text] The text to lint (used for TTY).
  265. * @param {boolean} [allowFlatConfig] Whether or not to allow flat config.
  266. * @returns {Promise<number>} The exit code for the operation.
  267. */
  268. async execute(args, text, allowFlatConfig) {
  269. if (Array.isArray(args)) {
  270. debug("CLI args: %o", args.slice(2));
  271. }
  272. /*
  273. * Before doing anything, we need to see if we are using a
  274. * flat config file. If so, then we need to change the way command
  275. * line args are parsed. This is temporary, and when we fully
  276. * switch to flat config we can remove this logic.
  277. */
  278. const usingFlatConfig = allowFlatConfig && await shouldUseFlatConfig();
  279. debug("Using flat config?", usingFlatConfig);
  280. const CLIOptions = createCLIOptions(usingFlatConfig);
  281. /** @type {ParsedCLIOptions} */
  282. let options;
  283. try {
  284. options = CLIOptions.parse(args);
  285. } catch (error) {
  286. debug("Error parsing CLI options:", error.message);
  287. let errorMessage = error.message;
  288. if (usingFlatConfig) {
  289. 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.";
  290. }
  291. log.error(errorMessage);
  292. return 2;
  293. }
  294. const files = options._;
  295. const useStdin = typeof text === "string";
  296. if (options.help) {
  297. log.info(CLIOptions.generateHelp());
  298. return 0;
  299. }
  300. if (options.version) {
  301. log.info(RuntimeInfo.version());
  302. return 0;
  303. }
  304. if (options.envInfo) {
  305. try {
  306. log.info(RuntimeInfo.environment());
  307. return 0;
  308. } catch (err) {
  309. debug("Error retrieving environment info");
  310. log.error(err.message);
  311. return 2;
  312. }
  313. }
  314. if (options.printConfig) {
  315. if (files.length) {
  316. log.error("The --print-config option must be used with exactly one file name.");
  317. return 2;
  318. }
  319. if (useStdin) {
  320. log.error("The --print-config option is not available for piped-in code.");
  321. return 2;
  322. }
  323. const engine = usingFlatConfig
  324. ? new FlatESLint(await translateOptions(options, "flat"))
  325. : new ESLint(await translateOptions(options));
  326. const fileConfig =
  327. await engine.calculateConfigForFile(options.printConfig);
  328. log.info(JSON.stringify(fileConfig, null, " "));
  329. return 0;
  330. }
  331. debug(`Running on ${useStdin ? "text" : "files"}`);
  332. if (options.fix && options.fixDryRun) {
  333. log.error("The --fix option and the --fix-dry-run option cannot be used together.");
  334. return 2;
  335. }
  336. if (useStdin && options.fix) {
  337. log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead.");
  338. return 2;
  339. }
  340. if (options.fixType && !options.fix && !options.fixDryRun) {
  341. log.error("The --fix-type option requires either --fix or --fix-dry-run.");
  342. return 2;
  343. }
  344. if (options.reportUnusedDisableDirectives && options.reportUnusedDisableDirectivesSeverity !== void 0) {
  345. log.error("The --report-unused-disable-directives option and the --report-unused-disable-directives-severity option cannot be used together.");
  346. return 2;
  347. }
  348. const ActiveESLint = usingFlatConfig ? FlatESLint : ESLint;
  349. const engine = new ActiveESLint(await translateOptions(options, usingFlatConfig ? "flat" : "eslintrc"));
  350. let results;
  351. if (useStdin) {
  352. results = await engine.lintText(text, {
  353. filePath: options.stdinFilename,
  354. // flatConfig respects CLI flag and constructor warnIgnored, eslintrc forces true for backwards compatibility
  355. warnIgnored: usingFlatConfig ? void 0 : true
  356. });
  357. } else {
  358. results = await engine.lintFiles(files);
  359. }
  360. if (options.fix) {
  361. debug("Fix mode enabled - applying fixes");
  362. await ActiveESLint.outputFixes(results);
  363. }
  364. let resultsToPrint = results;
  365. if (options.quiet) {
  366. debug("Quiet mode enabled - filtering out warnings");
  367. resultsToPrint = ActiveESLint.getErrorResults(resultsToPrint);
  368. }
  369. const resultCounts = countErrors(results);
  370. const tooManyWarnings = options.maxWarnings >= 0 && resultCounts.warningCount > options.maxWarnings;
  371. const resultsMeta = tooManyWarnings
  372. ? {
  373. maxWarningsExceeded: {
  374. maxWarnings: options.maxWarnings,
  375. foundWarnings: resultCounts.warningCount
  376. }
  377. }
  378. : {};
  379. if (await printResults(engine, resultsToPrint, options.format, options.outputFile, resultsMeta)) {
  380. // Errors and warnings from the original unfiltered results should determine the exit code
  381. const shouldExitForFatalErrors =
  382. options.exitOnFatalError && resultCounts.fatalErrorCount > 0;
  383. if (!resultCounts.errorCount && tooManyWarnings) {
  384. log.error(
  385. "ESLint found too many warnings (maximum: %s).",
  386. options.maxWarnings
  387. );
  388. }
  389. if (shouldExitForFatalErrors) {
  390. return 2;
  391. }
  392. return (resultCounts.errorCount || tooManyWarnings) ? 1 : 0;
  393. }
  394. return 2;
  395. }
  396. };
  397. module.exports = cli;