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 | { 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 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | const mkdir = promisify(fs.mkdir);
|
47 | const stat = promisify(fs.stat);
|
48 | const writeFile = promisify(fs.writeFile);
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | function quietFixPredicate(message) {
|
58 | return message.severity === 2;
|
59 | }
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
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 |
|
214 |
|
215 |
|
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 |
|
233 |
|
234 |
|
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 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
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 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 | const cli = {
|
302 |
|
303 | |
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 | async execute(args, text, allowFlatConfig) {
|
311 | if (Array.isArray(args)) {
|
312 | debug("CLI args: %o", args.slice(2));
|
313 | }
|
314 |
|
315 | |
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 | const usingFlatConfig = allowFlatConfig && await shouldUseFlatConfig();
|
323 |
|
324 | debug("Using flat config?", usingFlatConfig);
|
325 |
|
326 | const CLIOptions = createCLIOptions(usingFlatConfig);
|
327 |
|
328 |
|
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 |
|
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 |
|
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;
|