blob: 477310da5850e7c488adcc998c1e3d7985fd215e [file] [log] [blame]
Yang Guo4fd355c2019-09-19 10:59:03 +02001/**
2 * @fileoverview Main CLI object.
3 * @author Nicholas C. Zakas
4 */
5
6"use strict";
7
8/*
9 * The CLI object should *not* call process.exit() directly. It should only return
10 * exit codes. This allows other programs to use the CLI object and still control
11 * when the program exits.
12 */
13
14//------------------------------------------------------------------------------
15// Requirements
16//------------------------------------------------------------------------------
17
18const fs = require("fs"),
19 path = require("path"),
Tim van der Lippe16aca392020-11-13 11:37:13 +000020 { promisify } = require("util"),
21 { ESLint } = require("./eslint"),
22 CLIOptions = require("./options"),
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010023 log = require("./shared/logging"),
24 RuntimeInfo = require("./shared/runtime-info");
Yang Guo4fd355c2019-09-19 10:59:03 +020025
26const debug = require("debug")("eslint:cli");
27
28//------------------------------------------------------------------------------
Tim van der Lippe16aca392020-11-13 11:37:13 +000029// Types
30//------------------------------------------------------------------------------
31
32/** @typedef {import("./eslint/eslint").ESLintOptions} ESLintOptions */
33/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
34/** @typedef {import("./eslint/eslint").LintResult} LintResult */
Simon Zünd52e20202021-06-16 08:34:28 +020035/** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
Tim van der Lippe16aca392020-11-13 11:37:13 +000036
37//------------------------------------------------------------------------------
Yang Guo4fd355c2019-09-19 10:59:03 +020038// Helpers
39//------------------------------------------------------------------------------
40
Tim van der Lippe16aca392020-11-13 11:37:13 +000041const mkdir = promisify(fs.mkdir);
42const stat = promisify(fs.stat);
43const writeFile = promisify(fs.writeFile);
44
Yang Guo4fd355c2019-09-19 10:59:03 +020045/**
46 * Predicate function for whether or not to apply fixes in quiet mode.
47 * If a message is a warning, do not apply a fix.
Tim van der Lippe16aca392020-11-13 11:37:13 +000048 * @param {LintMessage} message The lint result.
Yang Guo4fd355c2019-09-19 10:59:03 +020049 * @returns {boolean} True if the lint message is an error (and thus should be
50 * autofixed), false otherwise.
51 */
Tim van der Lippe16aca392020-11-13 11:37:13 +000052function quietFixPredicate(message) {
53 return message.severity === 2;
Yang Guo4fd355c2019-09-19 10:59:03 +020054}
55
56/**
57 * Translates the CLI options into the options expected by the CLIEngine.
Simon Zünd52e20202021-06-16 08:34:28 +020058 * @param {ParsedCLIOptions} cliOptions The CLI options to translate.
Tim van der Lippe16aca392020-11-13 11:37:13 +000059 * @returns {ESLintOptions} The options object for the CLIEngine.
Yang Guo4fd355c2019-09-19 10:59:03 +020060 * @private
61 */
Tim van der Lippe16aca392020-11-13 11:37:13 +000062function translateOptions({
63 cache,
64 cacheFile,
65 cacheLocation,
Tim van der Lippe0a9b84d2021-03-24 11:53:15 +000066 cacheStrategy,
Tim van der Lippe16aca392020-11-13 11:37:13 +000067 config,
68 env,
69 errorOnUnmatchedPattern,
70 eslintrc,
71 ext,
72 fix,
73 fixDryRun,
74 fixType,
75 global,
76 ignore,
77 ignorePath,
78 ignorePattern,
79 inlineConfig,
80 parser,
81 parserOptions,
82 plugin,
83 quiet,
84 reportUnusedDisableDirectives,
85 resolvePluginsRelativeTo,
86 rule,
87 rulesdir
88}) {
Yang Guo4fd355c2019-09-19 10:59:03 +020089 return {
Tim van der Lippe16aca392020-11-13 11:37:13 +000090 allowInlineConfig: inlineConfig,
91 cache,
92 cacheLocation: cacheLocation || cacheFile,
Tim van der Lippe0a9b84d2021-03-24 11:53:15 +000093 cacheStrategy,
Tim van der Lippe16aca392020-11-13 11:37:13 +000094 errorOnUnmatchedPattern,
95 extensions: ext,
96 fix: (fix || fixDryRun) && (quiet ? quietFixPredicate : true),
97 fixTypes: fixType,
98 ignore,
99 ignorePath,
100 overrideConfig: {
101 env: env && env.reduce((obj, name) => {
102 obj[name] = true;
103 return obj;
104 }, {}),
105 globals: global && global.reduce((obj, name) => {
106 if (name.endsWith(":true")) {
107 obj[name.slice(0, -5)] = "writable";
108 } else {
109 obj[name] = "readonly";
110 }
111 return obj;
112 }, {}),
113 ignorePatterns: ignorePattern,
114 parser,
115 parserOptions,
116 plugins: plugin,
117 rules: rule
118 },
119 overrideConfigFile: config,
120 reportUnusedDisableDirectives: reportUnusedDisableDirectives ? "error" : void 0,
121 resolvePluginsRelativeTo,
122 rulePaths: rulesdir,
123 useEslintrc: eslintrc
Yang Guo4fd355c2019-09-19 10:59:03 +0200124 };
125}
126
127/**
Tim van der Lippe16aca392020-11-13 11:37:13 +0000128 * Count error messages.
129 * @param {LintResult[]} results The lint results.
130 * @returns {{errorCount:number;warningCount:number}} The number of error messages.
131 */
132function countErrors(results) {
133 let errorCount = 0;
Tim van der Lippea6619412021-09-13 14:28:55 +0200134 let fatalErrorCount = 0;
Tim van der Lippe16aca392020-11-13 11:37:13 +0000135 let warningCount = 0;
136
137 for (const result of results) {
138 errorCount += result.errorCount;
Tim van der Lippea6619412021-09-13 14:28:55 +0200139 fatalErrorCount += result.fatalErrorCount;
Tim van der Lippe16aca392020-11-13 11:37:13 +0000140 warningCount += result.warningCount;
141 }
142
Tim van der Lippea6619412021-09-13 14:28:55 +0200143 return { errorCount, fatalErrorCount, warningCount };
Tim van der Lippe16aca392020-11-13 11:37:13 +0000144}
145
146/**
147 * Check if a given file path is a directory or not.
148 * @param {string} filePath The path to a file to check.
149 * @returns {Promise<boolean>} `true` if the given path is a directory.
150 */
151async function isDirectory(filePath) {
152 try {
153 return (await stat(filePath)).isDirectory();
154 } catch (error) {
155 if (error.code === "ENOENT" || error.code === "ENOTDIR") {
156 return false;
157 }
158 throw error;
159 }
160}
161
162/**
Yang Guo4fd355c2019-09-19 10:59:03 +0200163 * Outputs the results of the linting.
Tim van der Lippe16aca392020-11-13 11:37:13 +0000164 * @param {ESLint} engine The ESLint instance to use.
Yang Guo4fd355c2019-09-19 10:59:03 +0200165 * @param {LintResult[]} results The results to print.
166 * @param {string} format The name of the formatter to use or the path to the formatter.
167 * @param {string} outputFile The path for the output file.
Tim van der Lippe16aca392020-11-13 11:37:13 +0000168 * @returns {Promise<boolean>} True if the printing succeeds, false if not.
Yang Guo4fd355c2019-09-19 10:59:03 +0200169 * @private
170 */
Tim van der Lippe16aca392020-11-13 11:37:13 +0000171async function printResults(engine, results, format, outputFile) {
Yang Guo4fd355c2019-09-19 10:59:03 +0200172 let formatter;
Yang Guo4fd355c2019-09-19 10:59:03 +0200173
174 try {
Tim van der Lippe16aca392020-11-13 11:37:13 +0000175 formatter = await engine.loadFormatter(format);
Yang Guo4fd355c2019-09-19 10:59:03 +0200176 } catch (e) {
177 log.error(e.message);
178 return false;
179 }
180
Tim van der Lippe16aca392020-11-13 11:37:13 +0000181 const output = formatter.format(results);
Yang Guo4fd355c2019-09-19 10:59:03 +0200182
183 if (output) {
184 if (outputFile) {
185 const filePath = path.resolve(process.cwd(), outputFile);
186
Tim van der Lippe16aca392020-11-13 11:37:13 +0000187 if (await isDirectory(filePath)) {
Yang Guo4fd355c2019-09-19 10:59:03 +0200188 log.error("Cannot write to output file path, it is a directory: %s", outputFile);
189 return false;
190 }
191
192 try {
Tim van der Lippe16aca392020-11-13 11:37:13 +0000193 await mkdir(path.dirname(filePath), { recursive: true });
194 await writeFile(filePath, output);
Yang Guo4fd355c2019-09-19 10:59:03 +0200195 } catch (ex) {
196 log.error("There was a problem writing the output file:\n%s", ex);
197 return false;
198 }
199 } else {
200 log.info(output);
201 }
202 }
203
204 return true;
Yang Guo4fd355c2019-09-19 10:59:03 +0200205}
206
207//------------------------------------------------------------------------------
208// Public Interface
209//------------------------------------------------------------------------------
210
211/**
212 * Encapsulates all CLI behavior for eslint. Makes it easier to test as well as
213 * for other Node.js programs to effectively run the CLI.
214 */
215const cli = {
216
217 /**
218 * Executes the CLI based on an array of arguments that is passed in.
219 * @param {string|Array|Object} args The arguments to process.
220 * @param {string} [text] The text to lint (used for TTY).
Tim van der Lippe16aca392020-11-13 11:37:13 +0000221 * @returns {Promise<number>} The exit code for the operation.
Yang Guo4fd355c2019-09-19 10:59:03 +0200222 */
Tim van der Lippe16aca392020-11-13 11:37:13 +0000223 async execute(args, text) {
Yang Guo4fd355c2019-09-19 10:59:03 +0200224 if (Array.isArray(args)) {
225 debug("CLI args: %o", args.slice(2));
226 }
Simon Zünd52e20202021-06-16 08:34:28 +0200227
228 /** @type {ParsedCLIOptions} */
Tim van der Lippe16aca392020-11-13 11:37:13 +0000229 let options;
Yang Guo4fd355c2019-09-19 10:59:03 +0200230
231 try {
Tim van der Lippe16aca392020-11-13 11:37:13 +0000232 options = CLIOptions.parse(args);
Yang Guo4fd355c2019-09-19 10:59:03 +0200233 } catch (error) {
234 log.error(error.message);
235 return 2;
236 }
237
Tim van der Lippe16aca392020-11-13 11:37:13 +0000238 const files = options._;
Yang Guo4fd355c2019-09-19 10:59:03 +0200239 const useStdin = typeof text === "string";
240
Tim van der Lippe16aca392020-11-13 11:37:13 +0000241 if (options.help) {
242 log.info(CLIOptions.generateHelp());
243 return 0;
244 }
245 if (options.version) {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100246 log.info(RuntimeInfo.version());
Tim van der Lippe16aca392020-11-13 11:37:13 +0000247 return 0;
248 }
249 if (options.envInfo) {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100250 try {
251 log.info(RuntimeInfo.environment());
252 return 0;
253 } catch (err) {
254 log.error(err.message);
255 return 2;
256 }
Tim van der Lippe16aca392020-11-13 11:37:13 +0000257 }
258
259 if (options.printConfig) {
Yang Guo4fd355c2019-09-19 10:59:03 +0200260 if (files.length) {
261 log.error("The --print-config option must be used with exactly one file name.");
262 return 2;
263 }
264 if (useStdin) {
265 log.error("The --print-config option is not available for piped-in code.");
266 return 2;
267 }
268
Tim van der Lippe16aca392020-11-13 11:37:13 +0000269 const engine = new ESLint(translateOptions(options));
270 const fileConfig =
271 await engine.calculateConfigForFile(options.printConfig);
Yang Guo4fd355c2019-09-19 10:59:03 +0200272
273 log.info(JSON.stringify(fileConfig, null, " "));
274 return 0;
Tim van der Lippe16aca392020-11-13 11:37:13 +0000275 }
Yang Guo4fd355c2019-09-19 10:59:03 +0200276
Tim van der Lippe16aca392020-11-13 11:37:13 +0000277 debug(`Running on ${useStdin ? "text" : "files"}`);
Yang Guo4fd355c2019-09-19 10:59:03 +0200278
Tim van der Lippe16aca392020-11-13 11:37:13 +0000279 if (options.fix && options.fixDryRun) {
280 log.error("The --fix option and the --fix-dry-run option cannot be used together.");
281 return 2;
282 }
283 if (useStdin && options.fix) {
284 log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead.");
285 return 2;
286 }
287 if (options.fixType && !options.fix && !options.fixDryRun) {
288 log.error("The --fix-type option requires either --fix or --fix-dry-run.");
Yang Guo4fd355c2019-09-19 10:59:03 +0200289 return 2;
Yang Guo4fd355c2019-09-19 10:59:03 +0200290 }
291
Tim van der Lippe16aca392020-11-13 11:37:13 +0000292 const engine = new ESLint(translateOptions(options));
293 let results;
294
295 if (useStdin) {
296 results = await engine.lintText(text, {
297 filePath: options.stdinFilename,
298 warnIgnored: true
299 });
300 } else {
301 results = await engine.lintFiles(files);
302 }
303
304 if (options.fix) {
305 debug("Fix mode enabled - applying fixes");
306 await ESLint.outputFixes(results);
307 }
308
Simon Zünd52e20202021-06-16 08:34:28 +0200309 let resultsToPrint = results;
310
Tim van der Lippe16aca392020-11-13 11:37:13 +0000311 if (options.quiet) {
312 debug("Quiet mode enabled - filtering out warnings");
Simon Zünd52e20202021-06-16 08:34:28 +0200313 resultsToPrint = ESLint.getErrorResults(resultsToPrint);
Tim van der Lippe16aca392020-11-13 11:37:13 +0000314 }
315
Simon Zünd52e20202021-06-16 08:34:28 +0200316 if (await printResults(engine, resultsToPrint, options.format, options.outputFile)) {
317
318 // Errors and warnings from the original unfiltered results should determine the exit code
Tim van der Lippea6619412021-09-13 14:28:55 +0200319 const { errorCount, fatalErrorCount, warningCount } = countErrors(results);
320
Tim van der Lippe16aca392020-11-13 11:37:13 +0000321 const tooManyWarnings =
322 options.maxWarnings >= 0 && warningCount > options.maxWarnings;
Tim van der Lippea6619412021-09-13 14:28:55 +0200323 const shouldExitForFatalErrors =
324 options.exitOnFatalError && fatalErrorCount > 0;
Tim van der Lippe16aca392020-11-13 11:37:13 +0000325
326 if (!errorCount && tooManyWarnings) {
327 log.error(
328 "ESLint found too many warnings (maximum: %s).",
329 options.maxWarnings
330 );
331 }
332
Tim van der Lippea6619412021-09-13 14:28:55 +0200333 if (shouldExitForFatalErrors) {
334 return 2;
335 }
336
Tim van der Lippe16aca392020-11-13 11:37:13 +0000337 return (errorCount || tooManyWarnings) ? 1 : 0;
338 }
339
340 return 2;
Yang Guo4fd355c2019-09-19 10:59:03 +0200341 }
342};
343
344module.exports = cli;