1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | "use strict";
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | const fs = require("fs"),
|
19 | path = require("path"),
|
20 | { promisify } = require("util"),
|
21 | { LegacyESLint } = require("./eslint"),
|
22 | { ESLint, shouldUseFlatConfig, locateConfigFileToUse } = require("./eslint/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 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 | const mkdir = promisify(fs.mkdir);
|
48 | const stat = promisify(fs.stat);
|
49 | const writeFile = promisify(fs.writeFile);
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | async function loadPlugins(importer, pluginNames) {
|
58 | const plugins = {};
|
59 |
|
60 | await Promise.all(pluginNames.map(async pluginName => {
|
61 |
|
62 | const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
|
63 | const module = await importer.import(longName);
|
64 |
|
65 | if (!("default" in module)) {
|
66 | throw new Error(`"${longName}" cannot be used with the \`--plugin\` option because its default module does not provide a \`default\` export`);
|
67 | }
|
68 |
|
69 | const shortName = naming.getShorthandName(pluginName, "eslint-plugin");
|
70 |
|
71 | plugins[shortName] = module.default;
|
72 | }));
|
73 |
|
74 | return plugins;
|
75 | }
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 | function quietFixPredicate(message) {
|
85 | return message.severity === 2;
|
86 | }
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 | function quietRuleFilter(rule) {
|
95 | return rule.severity === 2;
|
96 | }
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | async function translateOptions({
|
107 | cache,
|
108 | cacheFile,
|
109 | cacheLocation,
|
110 | cacheStrategy,
|
111 | config,
|
112 | configLookup,
|
113 | env,
|
114 | errorOnUnmatchedPattern,
|
115 | eslintrc,
|
116 | ext,
|
117 | fix,
|
118 | fixDryRun,
|
119 | fixType,
|
120 | global,
|
121 | ignore,
|
122 | ignorePath,
|
123 | ignorePattern,
|
124 | inlineConfig,
|
125 | parser,
|
126 | parserOptions,
|
127 | plugin,
|
128 | quiet,
|
129 | reportUnusedDisableDirectives,
|
130 | reportUnusedDisableDirectivesSeverity,
|
131 | resolvePluginsRelativeTo,
|
132 | rule,
|
133 | rulesdir,
|
134 | stats,
|
135 | warnIgnored,
|
136 | passOnNoPatterns,
|
137 | maxWarnings
|
138 | }, configType) {
|
139 |
|
140 | let overrideConfig, overrideConfigFile;
|
141 | const importer = new ModuleImporter();
|
142 |
|
143 | if (configType === "flat") {
|
144 | overrideConfigFile = (typeof config === "string") ? config : !configLookup;
|
145 | if (overrideConfigFile === false) {
|
146 | overrideConfigFile = void 0;
|
147 | }
|
148 |
|
149 | let globals = {};
|
150 |
|
151 | if (global) {
|
152 | globals = global.reduce((obj, name) => {
|
153 | if (name.endsWith(":true")) {
|
154 | obj[name.slice(0, -5)] = "writable";
|
155 | } else {
|
156 | obj[name] = "readonly";
|
157 | }
|
158 | return obj;
|
159 | }, globals);
|
160 | }
|
161 |
|
162 | overrideConfig = [{
|
163 | languageOptions: {
|
164 | globals,
|
165 | parserOptions: parserOptions || {}
|
166 | },
|
167 | rules: rule ? rule : {}
|
168 | }];
|
169 |
|
170 | if (reportUnusedDisableDirectives || reportUnusedDisableDirectivesSeverity !== void 0) {
|
171 | overrideConfig[0].linterOptions = {
|
172 | reportUnusedDisableDirectives: reportUnusedDisableDirectives
|
173 | ? "error"
|
174 | : normalizeSeverityToString(reportUnusedDisableDirectivesSeverity)
|
175 | };
|
176 | }
|
177 |
|
178 | if (parser) {
|
179 | overrideConfig[0].languageOptions.parser = await importer.import(parser);
|
180 | }
|
181 |
|
182 | if (plugin) {
|
183 | overrideConfig[0].plugins = await loadPlugins(importer, plugin);
|
184 | }
|
185 |
|
186 | } else {
|
187 | overrideConfigFile = config;
|
188 |
|
189 | overrideConfig = {
|
190 | env: env && env.reduce((obj, name) => {
|
191 | obj[name] = true;
|
192 | return obj;
|
193 | }, {}),
|
194 | globals: global && global.reduce((obj, name) => {
|
195 | if (name.endsWith(":true")) {
|
196 | obj[name.slice(0, -5)] = "writable";
|
197 | } else {
|
198 | obj[name] = "readonly";
|
199 | }
|
200 | return obj;
|
201 | }, {}),
|
202 | ignorePatterns: ignorePattern,
|
203 | parser,
|
204 | parserOptions,
|
205 | plugins: plugin,
|
206 | rules: rule
|
207 | };
|
208 | }
|
209 |
|
210 | const options = {
|
211 | allowInlineConfig: inlineConfig,
|
212 | cache,
|
213 | cacheLocation: cacheLocation || cacheFile,
|
214 | cacheStrategy,
|
215 | errorOnUnmatchedPattern,
|
216 | fix: (fix || fixDryRun) && (quiet ? quietFixPredicate : true),
|
217 | fixTypes: fixType,
|
218 | ignore,
|
219 | overrideConfig,
|
220 | overrideConfigFile,
|
221 | passOnNoPatterns
|
222 | };
|
223 |
|
224 | if (configType === "flat") {
|
225 | options.ignorePatterns = ignorePattern;
|
226 | options.stats = stats;
|
227 | options.warnIgnored = warnIgnored;
|
228 |
|
229 | |
230 |
|
231 |
|
232 |
|
233 | options.ruleFilter = quiet && maxWarnings === -1 ? quietRuleFilter : () => true;
|
234 | } else {
|
235 | options.resolvePluginsRelativeTo = resolvePluginsRelativeTo;
|
236 | options.rulePaths = rulesdir;
|
237 | options.useEslintrc = eslintrc;
|
238 | options.extensions = ext;
|
239 | options.ignorePath = ignorePath;
|
240 | if (reportUnusedDisableDirectives || reportUnusedDisableDirectivesSeverity !== void 0) {
|
241 | options.reportUnusedDisableDirectives = reportUnusedDisableDirectives
|
242 | ? "error"
|
243 | : normalizeSeverityToString(reportUnusedDisableDirectivesSeverity);
|
244 | }
|
245 | }
|
246 |
|
247 | return options;
|
248 | }
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 | function countErrors(results) {
|
256 | let errorCount = 0;
|
257 | let fatalErrorCount = 0;
|
258 | let warningCount = 0;
|
259 |
|
260 | for (const result of results) {
|
261 | errorCount += result.errorCount;
|
262 | fatalErrorCount += result.fatalErrorCount;
|
263 | warningCount += result.warningCount;
|
264 | }
|
265 |
|
266 | return { errorCount, fatalErrorCount, warningCount };
|
267 | }
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 | async function isDirectory(filePath) {
|
275 | try {
|
276 | return (await stat(filePath)).isDirectory();
|
277 | } catch (error) {
|
278 | if (error.code === "ENOENT" || error.code === "ENOTDIR") {
|
279 | return false;
|
280 | }
|
281 | throw error;
|
282 | }
|
283 | }
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 | async function printResults(engine, results, format, outputFile, resultsMeta) {
|
296 | let formatter;
|
297 |
|
298 | try {
|
299 | formatter = await engine.loadFormatter(format);
|
300 | } catch (e) {
|
301 | log.error(e.message);
|
302 | return false;
|
303 | }
|
304 |
|
305 | const output = await formatter.format(results, resultsMeta);
|
306 |
|
307 | if (outputFile) {
|
308 | const filePath = path.resolve(process.cwd(), outputFile);
|
309 |
|
310 | if (await isDirectory(filePath)) {
|
311 | log.error("Cannot write to output file path, it is a directory: %s", outputFile);
|
312 | return false;
|
313 | }
|
314 |
|
315 | try {
|
316 | await mkdir(path.dirname(filePath), { recursive: true });
|
317 | await writeFile(filePath, output);
|
318 | } catch (ex) {
|
319 | log.error("There was a problem writing the output file:\n%s", ex);
|
320 | return false;
|
321 | }
|
322 | } else if (output) {
|
323 | log.info(output);
|
324 | }
|
325 |
|
326 | return true;
|
327 | }
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
337 | const cli = {
|
338 |
|
339 | |
340 |
|
341 |
|
342 |
|
343 |
|
344 | async calculateInspectConfigFlags(configFile) {
|
345 |
|
346 |
|
347 | const {
|
348 | configFilePath,
|
349 | basePath,
|
350 | error
|
351 | } = await locateConfigFileToUse({ cwd: process.cwd(), configFile });
|
352 |
|
353 | if (error) {
|
354 | throw error;
|
355 | }
|
356 |
|
357 | return ["--config", configFilePath, "--basePath", basePath];
|
358 | },
|
359 |
|
360 | |
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 | async execute(args, text, allowFlatConfig = true) {
|
368 | if (Array.isArray(args)) {
|
369 | debug("CLI args: %o", args.slice(2));
|
370 | }
|
371 |
|
372 | |
373 |
|
374 |
|
375 |
|
376 |
|
377 |
|
378 |
|
379 | const usingFlatConfig = allowFlatConfig && await shouldUseFlatConfig();
|
380 |
|
381 | debug("Using flat config?", usingFlatConfig);
|
382 |
|
383 | if (allowFlatConfig && !usingFlatConfig) {
|
384 | process.emitWarning("You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details.", "ESLintRCWarning");
|
385 | }
|
386 |
|
387 | const CLIOptions = createCLIOptions(usingFlatConfig);
|
388 |
|
389 |
|
390 | let options;
|
391 |
|
392 | try {
|
393 | options = CLIOptions.parse(args);
|
394 | } catch (error) {
|
395 | debug("Error parsing CLI options:", error.message);
|
396 |
|
397 | let errorMessage = error.message;
|
398 |
|
399 | if (usingFlatConfig) {
|
400 | 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.";
|
401 | }
|
402 |
|
403 | log.error(errorMessage);
|
404 | return 2;
|
405 | }
|
406 |
|
407 | const files = options._;
|
408 | const useStdin = typeof text === "string";
|
409 |
|
410 | if (options.help) {
|
411 | log.info(CLIOptions.generateHelp());
|
412 | return 0;
|
413 | }
|
414 | if (options.version) {
|
415 | log.info(RuntimeInfo.version());
|
416 | return 0;
|
417 | }
|
418 | if (options.envInfo) {
|
419 | try {
|
420 | log.info(RuntimeInfo.environment());
|
421 | return 0;
|
422 | } catch (err) {
|
423 | debug("Error retrieving environment info");
|
424 | log.error(err.message);
|
425 | return 2;
|
426 | }
|
427 | }
|
428 |
|
429 | if (options.printConfig) {
|
430 | if (files.length) {
|
431 | log.error("The --print-config option must be used with exactly one file name.");
|
432 | return 2;
|
433 | }
|
434 | if (useStdin) {
|
435 | log.error("The --print-config option is not available for piped-in code.");
|
436 | return 2;
|
437 | }
|
438 |
|
439 | const engine = usingFlatConfig
|
440 | ? new ESLint(await translateOptions(options, "flat"))
|
441 | : new LegacyESLint(await translateOptions(options));
|
442 | const fileConfig =
|
443 | await engine.calculateConfigForFile(options.printConfig);
|
444 |
|
445 | log.info(JSON.stringify(fileConfig, null, " "));
|
446 | return 0;
|
447 | }
|
448 |
|
449 | if (options.inspectConfig) {
|
450 |
|
451 | log.info("You can also run this command directly using 'npx @eslint/config-inspector' in the same directory as your configuration file.");
|
452 |
|
453 | try {
|
454 | const flatOptions = await translateOptions(options, "flat");
|
455 | const spawn = require("cross-spawn");
|
456 | const flags = await cli.calculateInspectConfigFlags(flatOptions.overrideConfigFile);
|
457 |
|
458 | spawn.sync("npx", ["@eslint/config-inspector", ...flags], { encoding: "utf8", stdio: "inherit" });
|
459 | } catch (error) {
|
460 | log.error(error);
|
461 | return 2;
|
462 | }
|
463 |
|
464 | return 0;
|
465 | }
|
466 |
|
467 | debug(`Running on ${useStdin ? "text" : "files"}`);
|
468 |
|
469 | if (options.fix && options.fixDryRun) {
|
470 | log.error("The --fix option and the --fix-dry-run option cannot be used together.");
|
471 | return 2;
|
472 | }
|
473 | if (useStdin && options.fix) {
|
474 | log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead.");
|
475 | return 2;
|
476 | }
|
477 | if (options.fixType && !options.fix && !options.fixDryRun) {
|
478 | log.error("The --fix-type option requires either --fix or --fix-dry-run.");
|
479 | return 2;
|
480 | }
|
481 |
|
482 | if (options.reportUnusedDisableDirectives && options.reportUnusedDisableDirectivesSeverity !== void 0) {
|
483 | log.error("The --report-unused-disable-directives option and the --report-unused-disable-directives-severity option cannot be used together.");
|
484 | return 2;
|
485 | }
|
486 |
|
487 | const ActiveESLint = usingFlatConfig ? ESLint : LegacyESLint;
|
488 |
|
489 | const engine = new ActiveESLint(await translateOptions(options, usingFlatConfig ? "flat" : "eslintrc"));
|
490 | let results;
|
491 |
|
492 | if (useStdin) {
|
493 | results = await engine.lintText(text, {
|
494 | filePath: options.stdinFilename,
|
495 |
|
496 |
|
497 | warnIgnored: usingFlatConfig ? void 0 : true
|
498 | });
|
499 | } else {
|
500 | results = await engine.lintFiles(files);
|
501 | }
|
502 |
|
503 | if (options.fix) {
|
504 | debug("Fix mode enabled - applying fixes");
|
505 | await ActiveESLint.outputFixes(results);
|
506 | }
|
507 |
|
508 | let resultsToPrint = results;
|
509 |
|
510 | if (options.quiet) {
|
511 | debug("Quiet mode enabled - filtering out warnings");
|
512 | resultsToPrint = ActiveESLint.getErrorResults(resultsToPrint);
|
513 | }
|
514 |
|
515 | const resultCounts = countErrors(results);
|
516 | const tooManyWarnings = options.maxWarnings >= 0 && resultCounts.warningCount > options.maxWarnings;
|
517 | const resultsMeta = tooManyWarnings
|
518 | ? {
|
519 | maxWarningsExceeded: {
|
520 | maxWarnings: options.maxWarnings,
|
521 | foundWarnings: resultCounts.warningCount
|
522 | }
|
523 | }
|
524 | : {};
|
525 |
|
526 | if (await printResults(engine, resultsToPrint, options.format, options.outputFile, resultsMeta)) {
|
527 |
|
528 |
|
529 | const shouldExitForFatalErrors =
|
530 | options.exitOnFatalError && resultCounts.fatalErrorCount > 0;
|
531 |
|
532 | if (!resultCounts.errorCount && tooManyWarnings) {
|
533 | log.error(
|
534 | "ESLint found too many warnings (maximum: %s).",
|
535 | options.maxWarnings
|
536 | );
|
537 | }
|
538 |
|
539 | if (shouldExitForFatalErrors) {
|
540 | return 2;
|
541 | }
|
542 |
|
543 | return (resultCounts.errorCount || tooManyWarnings) ? 1 : 0;
|
544 | }
|
545 |
|
546 | return 2;
|
547 | }
|
548 | };
|
549 |
|
550 | module.exports = cli;
|