cli.js 14 KB

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