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