cli.js 14 KB

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