blob: f766764546cc5a002c4d13fddc2616b649e35494 [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;
134 let warningCount = 0;
135
136 for (const result of results) {
137 errorCount += result.errorCount;
138 warningCount += result.warningCount;
139 }
140
141 return { errorCount, warningCount };
142}
143
144/**
145 * Check if a given file path is a directory or not.
146 * @param {string} filePath The path to a file to check.
147 * @returns {Promise<boolean>} `true` if the given path is a directory.
148 */
149async function isDirectory(filePath) {
150 try {
151 return (await stat(filePath)).isDirectory();
152 } catch (error) {
153 if (error.code === "ENOENT" || error.code === "ENOTDIR") {
154 return false;
155 }
156 throw error;
157 }
158}
159
160/**
Yang Guo4fd355c2019-09-19 10:59:03 +0200161 * Outputs the results of the linting.
Tim van der Lippe16aca392020-11-13 11:37:13 +0000162 * @param {ESLint} engine The ESLint instance to use.
Yang Guo4fd355c2019-09-19 10:59:03 +0200163 * @param {LintResult[]} results The results to print.
164 * @param {string} format The name of the formatter to use or the path to the formatter.
165 * @param {string} outputFile The path for the output file.
Tim van der Lippe16aca392020-11-13 11:37:13 +0000166 * @returns {Promise<boolean>} True if the printing succeeds, false if not.
Yang Guo4fd355c2019-09-19 10:59:03 +0200167 * @private
168 */
Tim van der Lippe16aca392020-11-13 11:37:13 +0000169async function printResults(engine, results, format, outputFile) {
Yang Guo4fd355c2019-09-19 10:59:03 +0200170 let formatter;
Yang Guo4fd355c2019-09-19 10:59:03 +0200171
172 try {
Tim van der Lippe16aca392020-11-13 11:37:13 +0000173 formatter = await engine.loadFormatter(format);
Yang Guo4fd355c2019-09-19 10:59:03 +0200174 } catch (e) {
175 log.error(e.message);
176 return false;
177 }
178
Tim van der Lippe16aca392020-11-13 11:37:13 +0000179 const output = formatter.format(results);
Yang Guo4fd355c2019-09-19 10:59:03 +0200180
181 if (output) {
182 if (outputFile) {
183 const filePath = path.resolve(process.cwd(), outputFile);
184
Tim van der Lippe16aca392020-11-13 11:37:13 +0000185 if (await isDirectory(filePath)) {
Yang Guo4fd355c2019-09-19 10:59:03 +0200186 log.error("Cannot write to output file path, it is a directory: %s", outputFile);
187 return false;
188 }
189
190 try {
Tim van der Lippe16aca392020-11-13 11:37:13 +0000191 await mkdir(path.dirname(filePath), { recursive: true });
192 await writeFile(filePath, output);
Yang Guo4fd355c2019-09-19 10:59:03 +0200193 } catch (ex) {
194 log.error("There was a problem writing the output file:\n%s", ex);
195 return false;
196 }
197 } else {
198 log.info(output);
199 }
200 }
201
202 return true;
Yang Guo4fd355c2019-09-19 10:59:03 +0200203}
204
205//------------------------------------------------------------------------------
206// Public Interface
207//------------------------------------------------------------------------------
208
209/**
210 * Encapsulates all CLI behavior for eslint. Makes it easier to test as well as
211 * for other Node.js programs to effectively run the CLI.
212 */
213const cli = {
214
215 /**
216 * Executes the CLI based on an array of arguments that is passed in.
217 * @param {string|Array|Object} args The arguments to process.
218 * @param {string} [text] The text to lint (used for TTY).
Tim van der Lippe16aca392020-11-13 11:37:13 +0000219 * @returns {Promise<number>} The exit code for the operation.
Yang Guo4fd355c2019-09-19 10:59:03 +0200220 */
Tim van der Lippe16aca392020-11-13 11:37:13 +0000221 async execute(args, text) {
Yang Guo4fd355c2019-09-19 10:59:03 +0200222 if (Array.isArray(args)) {
223 debug("CLI args: %o", args.slice(2));
224 }
Simon Zünd52e20202021-06-16 08:34:28 +0200225
226 /** @type {ParsedCLIOptions} */
Tim van der Lippe16aca392020-11-13 11:37:13 +0000227 let options;
Yang Guo4fd355c2019-09-19 10:59:03 +0200228
229 try {
Tim van der Lippe16aca392020-11-13 11:37:13 +0000230 options = CLIOptions.parse(args);
Yang Guo4fd355c2019-09-19 10:59:03 +0200231 } catch (error) {
232 log.error(error.message);
233 return 2;
234 }
235
Tim van der Lippe16aca392020-11-13 11:37:13 +0000236 const files = options._;
Yang Guo4fd355c2019-09-19 10:59:03 +0200237 const useStdin = typeof text === "string";
238
Tim van der Lippe16aca392020-11-13 11:37:13 +0000239 if (options.help) {
240 log.info(CLIOptions.generateHelp());
241 return 0;
242 }
243 if (options.version) {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100244 log.info(RuntimeInfo.version());
Tim van der Lippe16aca392020-11-13 11:37:13 +0000245 return 0;
246 }
247 if (options.envInfo) {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100248 try {
249 log.info(RuntimeInfo.environment());
250 return 0;
251 } catch (err) {
252 log.error(err.message);
253 return 2;
254 }
Tim van der Lippe16aca392020-11-13 11:37:13 +0000255 }
256
257 if (options.printConfig) {
Yang Guo4fd355c2019-09-19 10:59:03 +0200258 if (files.length) {
259 log.error("The --print-config option must be used with exactly one file name.");
260 return 2;
261 }
262 if (useStdin) {
263 log.error("The --print-config option is not available for piped-in code.");
264 return 2;
265 }
266
Tim van der Lippe16aca392020-11-13 11:37:13 +0000267 const engine = new ESLint(translateOptions(options));
268 const fileConfig =
269 await engine.calculateConfigForFile(options.printConfig);
Yang Guo4fd355c2019-09-19 10:59:03 +0200270
271 log.info(JSON.stringify(fileConfig, null, " "));
272 return 0;
Tim van der Lippe16aca392020-11-13 11:37:13 +0000273 }
Yang Guo4fd355c2019-09-19 10:59:03 +0200274
Tim van der Lippe16aca392020-11-13 11:37:13 +0000275 debug(`Running on ${useStdin ? "text" : "files"}`);
Yang Guo4fd355c2019-09-19 10:59:03 +0200276
Tim van der Lippe16aca392020-11-13 11:37:13 +0000277 if (options.fix && options.fixDryRun) {
278 log.error("The --fix option and the --fix-dry-run option cannot be used together.");
279 return 2;
280 }
281 if (useStdin && options.fix) {
282 log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead.");
283 return 2;
284 }
285 if (options.fixType && !options.fix && !options.fixDryRun) {
286 log.error("The --fix-type option requires either --fix or --fix-dry-run.");
Yang Guo4fd355c2019-09-19 10:59:03 +0200287 return 2;
Yang Guo4fd355c2019-09-19 10:59:03 +0200288 }
289
Tim van der Lippe16aca392020-11-13 11:37:13 +0000290 const engine = new ESLint(translateOptions(options));
291 let results;
292
293 if (useStdin) {
294 results = await engine.lintText(text, {
295 filePath: options.stdinFilename,
296 warnIgnored: true
297 });
298 } else {
299 results = await engine.lintFiles(files);
300 }
301
302 if (options.fix) {
303 debug("Fix mode enabled - applying fixes");
304 await ESLint.outputFixes(results);
305 }
306
Simon Zünd52e20202021-06-16 08:34:28 +0200307 let resultsToPrint = results;
308
Tim van der Lippe16aca392020-11-13 11:37:13 +0000309 if (options.quiet) {
310 debug("Quiet mode enabled - filtering out warnings");
Simon Zünd52e20202021-06-16 08:34:28 +0200311 resultsToPrint = ESLint.getErrorResults(resultsToPrint);
Tim van der Lippe16aca392020-11-13 11:37:13 +0000312 }
313
Simon Zünd52e20202021-06-16 08:34:28 +0200314 if (await printResults(engine, resultsToPrint, options.format, options.outputFile)) {
315
316 // Errors and warnings from the original unfiltered results should determine the exit code
Tim van der Lippe16aca392020-11-13 11:37:13 +0000317 const { errorCount, warningCount } = countErrors(results);
318 const tooManyWarnings =
319 options.maxWarnings >= 0 && warningCount > options.maxWarnings;
320
321 if (!errorCount && tooManyWarnings) {
322 log.error(
323 "ESLint found too many warnings (maximum: %s).",
324 options.maxWarnings
325 );
326 }
327
328 return (errorCount || tooManyWarnings) ? 1 : 0;
329 }
330
331 return 2;
Yang Guo4fd355c2019-09-19 10:59:03 +0200332 }
333};
334
335module.exports = cli;