]>
Commit | Line | Data |
---|---|---|
8f9d1d4d DC |
1 | /** |
2 | * @fileoverview Main class using flat config | |
3 | * @author Nicholas C. Zakas | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | // Note: Node.js 12 does not support fs/promises. | |
13 | const fs = require("fs").promises; | |
14 | const path = require("path"); | |
15 | const findUp = require("find-up"); | |
16 | const { version } = require("../../package.json"); | |
17 | const { Linter } = require("../linter"); | |
18 | const { getRuleFromConfig } = require("../config/flat-config-helpers"); | |
19 | const { gitignoreToMinimatch } = require("@humanwhocodes/gitignore-to-minimatch"); | |
20 | const { | |
21 | Legacy: { | |
22 | ConfigOps: { | |
23 | getRuleSeverity | |
24 | }, | |
25 | ModuleResolver, | |
26 | naming | |
27 | } | |
28 | } = require("@eslint/eslintrc"); | |
29 | ||
30 | const { | |
31 | fileExists, | |
32 | findFiles, | |
33 | getCacheFile, | |
34 | ||
35 | isNonEmptyString, | |
36 | isArrayOfNonEmptyString, | |
37 | ||
38 | createIgnoreResult, | |
39 | isErrorMessage, | |
40 | ||
41 | processOptions | |
42 | } = require("./eslint-helpers"); | |
43 | const { pathToFileURL } = require("url"); | |
44 | const { FlatConfigArray } = require("../config/flat-config-array"); | |
45 | const LintResultCache = require("../cli-engine/lint-result-cache"); | |
46 | ||
47 | /* | |
48 | * This is necessary to allow overwriting writeFile for testing purposes. | |
49 | * We can just use fs/promises once we drop Node.js 12 support. | |
50 | */ | |
51 | ||
52 | //------------------------------------------------------------------------------ | |
53 | // Typedefs | |
54 | //------------------------------------------------------------------------------ | |
55 | ||
56 | // For VSCode IntelliSense | |
57 | /** @typedef {import("../shared/types").ConfigData} ConfigData */ | |
58 | /** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ | |
59 | /** @typedef {import("../shared/types").LintMessage} LintMessage */ | |
60 | /** @typedef {import("../shared/types").ParserOptions} ParserOptions */ | |
61 | /** @typedef {import("../shared/types").Plugin} Plugin */ | |
62 | /** @typedef {import("../shared/types").RuleConf} RuleConf */ | |
63 | /** @typedef {import("../shared/types").Rule} Rule */ | |
64 | /** @typedef {ReturnType<ConfigArray.extractConfig>} ExtractedConfig */ | |
65 | ||
66 | /** | |
67 | * The options with which to configure the ESLint instance. | |
68 | * @typedef {Object} FlatESLintOptions | |
69 | * @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments. | |
70 | * @property {ConfigData} [baseConfig] Base config object, extended by all configs used with this instance | |
71 | * @property {boolean} [cache] Enable result caching. | |
72 | * @property {string} [cacheLocation] The cache file to use instead of .eslintcache. | |
73 | * @property {"metadata" | "content"} [cacheStrategy] The strategy used to detect changed files. | |
74 | * @property {string} [cwd] The value to use for the current working directory. | |
75 | * @property {boolean} [errorOnUnmatchedPattern] If `false` then `ESLint#lintFiles()` doesn't throw even if no target files found. Defaults to `true`. | |
76 | * @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean. | |
77 | * @property {string[]} [fixTypes] Array of rule types to apply fixes for. | |
78 | * @property {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. | |
79 | * @property {boolean} [ignore] False disables use of .eslintignore. | |
80 | * @property {string} [ignorePath] The ignore file to use instead of .eslintignore. | |
81 | * @property {string[]} [ignorePatterns] Ignore file patterns to use in addition to .eslintignore. | |
82 | * @property {ConfigData} [overrideConfig] Override config object, overrides all configs used with this instance | |
83 | * @property {boolean|string} [overrideConfigFile] Searches for default config file when falsy; | |
84 | * doesn't do any config file lookup when `true`; considered to be a config filename | |
85 | * when a string. | |
86 | * @property {Record<string,Plugin>} [plugins] An array of plugin implementations. | |
87 | * @property {"error" | "warn" | "off"} [reportUnusedDisableDirectives] the severity to report unused eslint-disable directives. | |
88 | */ | |
89 | ||
90 | //------------------------------------------------------------------------------ | |
91 | // Helpers | |
92 | //------------------------------------------------------------------------------ | |
93 | ||
94 | const FLAT_CONFIG_FILENAME = "eslint.config.js"; | |
95 | const debug = require("debug")("eslint:flat-eslint"); | |
96 | const removedFormatters = new Set(["table", "codeframe"]); | |
97 | const privateMembers = new WeakMap(); | |
98 | ||
99 | /** | |
100 | * It will calculate the error and warning count for collection of messages per file | |
101 | * @param {LintMessage[]} messages Collection of messages | |
102 | * @returns {Object} Contains the stats | |
103 | * @private | |
104 | */ | |
105 | function calculateStatsPerFile(messages) { | |
106 | return messages.reduce((stat, message) => { | |
107 | if (message.fatal || message.severity === 2) { | |
108 | stat.errorCount++; | |
109 | if (message.fatal) { | |
110 | stat.fatalErrorCount++; | |
111 | } | |
112 | if (message.fix) { | |
113 | stat.fixableErrorCount++; | |
114 | } | |
115 | } else { | |
116 | stat.warningCount++; | |
117 | if (message.fix) { | |
118 | stat.fixableWarningCount++; | |
119 | } | |
120 | } | |
121 | return stat; | |
122 | }, { | |
123 | errorCount: 0, | |
124 | fatalErrorCount: 0, | |
125 | warningCount: 0, | |
126 | fixableErrorCount: 0, | |
127 | fixableWarningCount: 0 | |
128 | }); | |
129 | } | |
130 | ||
131 | /** | |
132 | * It will calculate the error and warning count for collection of results from all files | |
133 | * @param {LintResult[]} results Collection of messages from all the files | |
134 | * @returns {Object} Contains the stats | |
135 | * @private | |
136 | */ | |
137 | function calculateStatsPerRun(results) { | |
138 | return results.reduce((stat, result) => { | |
139 | stat.errorCount += result.errorCount; | |
140 | stat.fatalErrorCount += result.fatalErrorCount; | |
141 | stat.warningCount += result.warningCount; | |
142 | stat.fixableErrorCount += result.fixableErrorCount; | |
143 | stat.fixableWarningCount += result.fixableWarningCount; | |
144 | return stat; | |
145 | }, { | |
146 | errorCount: 0, | |
147 | fatalErrorCount: 0, | |
148 | warningCount: 0, | |
149 | fixableErrorCount: 0, | |
150 | fixableWarningCount: 0 | |
151 | }); | |
152 | } | |
153 | ||
154 | /** | |
155 | * Loads global ignore patterns from an ignore file (usually .eslintignore). | |
156 | * @param {string} filePath The filename to load. | |
157 | * @returns {ignore} A function encapsulating the ignore patterns. | |
158 | * @throws {Error} If the file cannot be read. | |
159 | * @private | |
160 | */ | |
161 | async function loadIgnoreFilePatterns(filePath) { | |
162 | debug(`Loading ignore file: ${filePath}`); | |
163 | ||
164 | try { | |
165 | const ignoreFileText = await fs.readFile(filePath, { encoding: "utf8" }); | |
166 | ||
167 | return ignoreFileText | |
168 | .split(/\r?\n/gu) | |
169 | .filter(line => line.trim() !== "" && !line.startsWith("#")); | |
170 | ||
171 | } catch (e) { | |
172 | debug(`Error reading ignore file: ${filePath}`); | |
173 | e.message = `Cannot read ignore file: ${filePath}\nError: ${e.message}`; | |
174 | throw e; | |
175 | } | |
176 | } | |
177 | ||
178 | /** | |
179 | * Create rulesMeta object. | |
180 | * @param {Map<string,Rule>} rules a map of rules from which to generate the object. | |
181 | * @returns {Object} metadata for all enabled rules. | |
182 | */ | |
183 | function createRulesMeta(rules) { | |
184 | return Array.from(rules).reduce((retVal, [id, rule]) => { | |
185 | retVal[id] = rule.meta; | |
186 | return retVal; | |
187 | }, {}); | |
188 | } | |
189 | ||
190 | /** @type {WeakMap<ExtractedConfig, DeprecatedRuleInfo[]>} */ | |
191 | const usedDeprecatedRulesCache = new WeakMap(); | |
192 | ||
193 | /** | |
194 | * Create used deprecated rule list. | |
195 | * @param {CLIEngine} eslint The CLIEngine instance. | |
196 | * @param {string} maybeFilePath The absolute path to a lint target file or `"<text>"`. | |
197 | * @returns {DeprecatedRuleInfo[]} The used deprecated rule list. | |
198 | */ | |
199 | function getOrFindUsedDeprecatedRules(eslint, maybeFilePath) { | |
200 | const { | |
201 | configs, | |
202 | options: { cwd } | |
203 | } = privateMembers.get(eslint); | |
204 | const filePath = path.isAbsolute(maybeFilePath) | |
205 | ? maybeFilePath | |
206 | : path.join(cwd, "__placeholder__.js"); | |
207 | const config = configs.getConfig(filePath); | |
208 | ||
209 | // Most files use the same config, so cache it. | |
210 | if (config && !usedDeprecatedRulesCache.has(config)) { | |
211 | const retv = []; | |
212 | ||
213 | if (config.rules) { | |
214 | for (const [ruleId, ruleConf] of Object.entries(config.rules)) { | |
215 | if (getRuleSeverity(ruleConf) === 0) { | |
216 | continue; | |
217 | } | |
218 | const rule = getRuleFromConfig(ruleId, config); | |
219 | const meta = rule && rule.meta; | |
220 | ||
221 | if (meta && meta.deprecated) { | |
222 | retv.push({ ruleId, replacedBy: meta.replacedBy || [] }); | |
223 | } | |
224 | } | |
225 | } | |
226 | ||
227 | ||
228 | usedDeprecatedRulesCache.set(config, Object.freeze(retv)); | |
229 | } | |
230 | ||
231 | return config ? usedDeprecatedRulesCache.get(config) : Object.freeze([]); | |
232 | } | |
233 | ||
234 | /** | |
235 | * Processes the linting results generated by a CLIEngine linting report to | |
236 | * match the ESLint class's API. | |
237 | * @param {CLIEngine} eslint The CLIEngine instance. | |
238 | * @param {CLIEngineLintReport} report The CLIEngine linting report to process. | |
239 | * @returns {LintResult[]} The processed linting results. | |
240 | */ | |
241 | function processLintReport(eslint, { results }) { | |
242 | const descriptor = { | |
243 | configurable: true, | |
244 | enumerable: true, | |
245 | get() { | |
246 | return getOrFindUsedDeprecatedRules(eslint, this.filePath); | |
247 | } | |
248 | }; | |
249 | ||
250 | for (const result of results) { | |
251 | Object.defineProperty(result, "usedDeprecatedRules", descriptor); | |
252 | } | |
253 | ||
254 | return results; | |
255 | } | |
256 | ||
257 | /** | |
258 | * An Array.prototype.sort() compatible compare function to order results by their file path. | |
259 | * @param {LintResult} a The first lint result. | |
260 | * @param {LintResult} b The second lint result. | |
261 | * @returns {number} An integer representing the order in which the two results should occur. | |
262 | */ | |
263 | function compareResultsByFilePath(a, b) { | |
264 | if (a.filePath < b.filePath) { | |
265 | return -1; | |
266 | } | |
267 | ||
268 | if (a.filePath > b.filePath) { | |
269 | return 1; | |
270 | } | |
271 | ||
272 | return 0; | |
273 | } | |
274 | ||
275 | /** | |
276 | * Searches from the current working directory up until finding the | |
277 | * given flat config filename. | |
278 | * @param {string} cwd The current working directory to search from. | |
279 | * @returns {Promise<string|null>} The filename if found or `null` if not. | |
280 | */ | |
281 | function findFlatConfigFile(cwd) { | |
282 | return findUp( | |
283 | FLAT_CONFIG_FILENAME, | |
284 | { cwd } | |
285 | ); | |
286 | } | |
287 | ||
288 | /** | |
289 | * Load the config array from the given filename. | |
290 | * @param {string} filePath The filename to load from. | |
291 | * @param {Object} options Options to help load the config file. | |
292 | * @param {string} options.basePath The base path for the config array. | |
293 | * @param {boolean} options.shouldIgnore Whether to honor ignore patterns. | |
294 | * @returns {Promise<FlatConfigArray>} The config array loaded from the config file. | |
295 | */ | |
296 | async function loadFlatConfigFile(filePath, { basePath, shouldIgnore }) { | |
297 | debug(`Loading config from ${filePath}`); | |
298 | ||
299 | const fileURL = pathToFileURL(filePath); | |
300 | ||
301 | debug(`Config file URL is ${fileURL}`); | |
302 | ||
303 | const module = await import(fileURL); | |
304 | ||
305 | return new FlatConfigArray(module.default, { | |
306 | basePath, | |
307 | shouldIgnore | |
308 | }); | |
309 | } | |
310 | ||
311 | /** | |
312 | * Calculates the config array for this run based on inputs. | |
313 | * @param {FlatESLint} eslint The instance to create the config array for. | |
314 | * @param {import("./eslint").ESLintOptions} options The ESLint instance options. | |
315 | * @returns {FlatConfigArray} The config array for `eslint``. | |
316 | */ | |
317 | async function calculateConfigArray(eslint, { | |
318 | cwd, | |
319 | overrideConfig, | |
320 | configFile, | |
321 | ignore: shouldIgnore, | |
322 | ignorePath, | |
323 | ignorePatterns | |
324 | }) { | |
325 | ||
326 | // check for cached instance | |
327 | const slots = privateMembers.get(eslint); | |
328 | ||
329 | if (slots.configs) { | |
330 | return slots.configs; | |
331 | } | |
332 | ||
333 | // determine where to load config file from | |
334 | let configFilePath; | |
335 | let basePath = cwd; | |
336 | ||
337 | if (typeof configFile === "string") { | |
338 | debug(`Override config file path is ${configFile}`); | |
339 | configFilePath = path.resolve(cwd, configFile); | |
340 | } else if (configFile !== false) { | |
341 | debug("Searching for eslint.config.js"); | |
342 | configFilePath = await findFlatConfigFile(cwd); | |
343 | ||
344 | if (!configFilePath) { | |
345 | throw new Error("Could not find config file."); | |
346 | } | |
347 | ||
348 | basePath = path.resolve(path.dirname(configFilePath)); | |
349 | } | |
350 | ||
351 | // load config array | |
352 | let configs; | |
353 | ||
354 | if (configFilePath) { | |
355 | configs = await loadFlatConfigFile(configFilePath, { | |
356 | basePath, | |
357 | shouldIgnore | |
358 | }); | |
359 | } else { | |
360 | configs = new FlatConfigArray([], { basePath, shouldIgnore }); | |
361 | } | |
362 | ||
363 | // add in any configured defaults | |
364 | configs.push(...slots.defaultConfigs); | |
365 | ||
366 | let allIgnorePatterns = []; | |
367 | let ignoreFilePath; | |
368 | ||
369 | // load ignore file if necessary | |
370 | if (shouldIgnore) { | |
371 | if (ignorePath) { | |
372 | ignoreFilePath = path.resolve(cwd, ignorePath); | |
373 | allIgnorePatterns = await loadIgnoreFilePatterns(ignoreFilePath); | |
374 | } else { | |
375 | ignoreFilePath = path.resolve(cwd, ".eslintignore"); | |
376 | ||
377 | // no error if .eslintignore doesn't exist` | |
378 | if (fileExists(ignoreFilePath)) { | |
379 | allIgnorePatterns = await loadIgnoreFilePatterns(ignoreFilePath); | |
380 | } | |
381 | } | |
382 | } | |
383 | ||
384 | // append command line ignore patterns | |
385 | if (ignorePatterns) { | |
386 | if (typeof ignorePatterns === "string") { | |
387 | allIgnorePatterns.push(ignorePatterns); | |
388 | } else { | |
389 | allIgnorePatterns.push(...ignorePatterns); | |
390 | } | |
391 | } | |
392 | ||
393 | /* | |
394 | * If the config file basePath is different than the cwd, then | |
395 | * the ignore patterns won't work correctly. Here, we adjust the | |
396 | * ignore pattern to include the correct relative path. Patterns | |
397 | * loaded from ignore files are always relative to the cwd, whereas | |
398 | * the config file basePath can be an ancestor of the cwd. | |
399 | */ | |
400 | if (basePath !== cwd && allIgnorePatterns.length) { | |
401 | ||
402 | const relativeIgnorePath = path.relative(basePath, cwd); | |
403 | ||
404 | allIgnorePatterns = allIgnorePatterns.map(pattern => { | |
405 | const negated = pattern.startsWith("!"); | |
406 | const basePattern = negated ? pattern.slice(1) : pattern; | |
407 | ||
408 | /* | |
409 | * Ignore patterns are considered relative to a directory | |
410 | * when the pattern contains a slash in a position other | |
411 | * than the last character. If that's the case, we need to | |
412 | * add the relative ignore path to the current pattern to | |
413 | * get the correct behavior. Otherwise, no change is needed. | |
414 | */ | |
415 | if (!basePattern.includes("/") || basePattern.endsWith("/")) { | |
416 | return pattern; | |
417 | } | |
418 | ||
419 | return (negated ? "!" : "") + | |
420 | path.posix.join(relativeIgnorePath, basePattern); | |
421 | }); | |
422 | } | |
423 | ||
424 | if (allIgnorePatterns.length) { | |
425 | ||
426 | /* | |
427 | * Ignore patterns are added to the end of the config array | |
428 | * so they can override default ignores. | |
429 | */ | |
430 | configs.push({ | |
431 | ignores: allIgnorePatterns.map(gitignoreToMinimatch) | |
432 | }); | |
433 | } | |
434 | ||
435 | if (overrideConfig) { | |
436 | if (Array.isArray(overrideConfig)) { | |
437 | configs.push(...overrideConfig); | |
438 | } else { | |
439 | configs.push(overrideConfig); | |
440 | } | |
441 | } | |
442 | ||
443 | await configs.normalize(); | |
444 | ||
445 | // cache the config array for this instance | |
446 | slots.configs = configs; | |
447 | ||
448 | return configs; | |
449 | } | |
450 | ||
451 | /** | |
452 | * Processes an source code using ESLint. | |
453 | * @param {Object} config The config object. | |
454 | * @param {string} config.text The source code to verify. | |
455 | * @param {string} config.cwd The path to the current working directory. | |
456 | * @param {string|undefined} config.filePath The path to the file of `text`. If this is undefined, it uses `<text>`. | |
457 | * @param {FlatConfigArray} config.configs The config. | |
458 | * @param {boolean} config.fix If `true` then it does fix. | |
459 | * @param {boolean} config.allowInlineConfig If `true` then it uses directive comments. | |
460 | * @param {boolean} config.reportUnusedDisableDirectives If `true` then it reports unused `eslint-disable` comments. | |
461 | * @param {Linter} config.linter The linter instance to verify. | |
462 | * @returns {LintResult} The result of linting. | |
463 | * @private | |
464 | */ | |
465 | function verifyText({ | |
466 | text, | |
467 | cwd, | |
468 | filePath: providedFilePath, | |
469 | configs, | |
470 | fix, | |
471 | allowInlineConfig, | |
472 | reportUnusedDisableDirectives, | |
473 | linter | |
474 | }) { | |
475 | const filePath = providedFilePath || "<text>"; | |
476 | ||
477 | debug(`Lint ${filePath}`); | |
478 | ||
479 | /* | |
480 | * Verify. | |
481 | * `config.extractConfig(filePath)` requires an absolute path, but `linter` | |
482 | * doesn't know CWD, so it gives `linter` an absolute path always. | |
483 | */ | |
484 | const filePathToVerify = filePath === "<text>" ? path.join(cwd, "__placeholder__.js") : filePath; | |
485 | const { fixed, messages, output } = linter.verifyAndFix( | |
486 | text, | |
487 | configs, | |
488 | { | |
489 | allowInlineConfig, | |
490 | filename: filePathToVerify, | |
491 | fix, | |
492 | reportUnusedDisableDirectives, | |
493 | ||
494 | /** | |
495 | * Check if the linter should adopt a given code block or not. | |
496 | * @param {string} blockFilename The virtual filename of a code block. | |
497 | * @returns {boolean} `true` if the linter should adopt the code block. | |
498 | */ | |
499 | filterCodeBlock(blockFilename) { | |
500 | return configs.isExplicitMatch(blockFilename); | |
501 | } | |
502 | } | |
503 | ); | |
504 | ||
505 | // Tweak and return. | |
506 | const result = { | |
507 | filePath: filePath === "<text>" ? filePath : path.resolve(filePath), | |
508 | messages, | |
509 | suppressedMessages: linter.getSuppressedMessages(), | |
510 | ...calculateStatsPerFile(messages) | |
511 | }; | |
512 | ||
513 | if (fixed) { | |
514 | result.output = output; | |
515 | } | |
516 | ||
517 | if ( | |
518 | result.errorCount + result.warningCount > 0 && | |
519 | typeof result.output === "undefined" | |
520 | ) { | |
521 | result.source = text; | |
522 | } | |
523 | ||
524 | return result; | |
525 | } | |
526 | ||
527 | /** | |
528 | * Checks whether a message's rule type should be fixed. | |
529 | * @param {LintMessage} message The message to check. | |
530 | * @param {FlatConfig} config The config for the file that generated the message. | |
531 | * @param {string[]} fixTypes An array of fix types to check. | |
532 | * @returns {boolean} Whether the message should be fixed. | |
533 | */ | |
534 | function shouldMessageBeFixed(message, config, fixTypes) { | |
535 | if (!message.ruleId) { | |
536 | return fixTypes.has("directive"); | |
537 | } | |
538 | ||
539 | const rule = message.ruleId && getRuleFromConfig(message.ruleId, config); | |
540 | ||
541 | return Boolean(rule && rule.meta && fixTypes.has(rule.meta.type)); | |
542 | } | |
543 | ||
544 | /** | |
545 | * Collect used deprecated rules. | |
546 | * @param {Array<FlatConfig>} configs The configs to evaluate. | |
547 | * @returns {IterableIterator<DeprecatedRuleInfo>} Used deprecated rules. | |
548 | */ | |
549 | function *iterateRuleDeprecationWarnings(configs) { | |
550 | const processedRuleIds = new Set(); | |
551 | ||
552 | for (const config of configs) { | |
553 | for (const [ruleId, ruleConfig] of Object.entries(config.rules)) { | |
554 | ||
555 | // Skip if it was processed. | |
556 | if (processedRuleIds.has(ruleId)) { | |
557 | continue; | |
558 | } | |
559 | processedRuleIds.add(ruleId); | |
560 | ||
561 | // Skip if it's not used. | |
562 | if (!getRuleSeverity(ruleConfig)) { | |
563 | continue; | |
564 | } | |
565 | const rule = getRuleFromConfig(ruleId, config); | |
566 | ||
567 | // Skip if it's not deprecated. | |
568 | if (!(rule && rule.meta && rule.meta.deprecated)) { | |
569 | continue; | |
570 | } | |
571 | ||
572 | // This rule was used and deprecated. | |
573 | yield { | |
574 | ruleId, | |
575 | replacedBy: rule.meta.replacedBy || [] | |
576 | }; | |
577 | } | |
578 | } | |
579 | } | |
580 | ||
581 | //----------------------------------------------------------------------------- | |
582 | // Main API | |
583 | //----------------------------------------------------------------------------- | |
584 | ||
585 | /** | |
586 | * Primary Node.js API for ESLint. | |
587 | */ | |
588 | class FlatESLint { | |
589 | ||
590 | /** | |
591 | * Creates a new instance of the main ESLint API. | |
592 | * @param {FlatESLintOptions} options The options for this instance. | |
593 | */ | |
594 | constructor(options = {}) { | |
595 | ||
596 | const defaultConfigs = []; | |
597 | const processedOptions = processOptions(options); | |
598 | const linter = new Linter({ | |
599 | cwd: processedOptions.cwd, | |
600 | configType: "flat" | |
601 | }); | |
602 | ||
603 | const cacheFilePath = getCacheFile( | |
604 | processedOptions.cacheLocation, | |
605 | processedOptions.cwd | |
606 | ); | |
607 | ||
608 | const lintResultCache = processedOptions.cache | |
609 | ? new LintResultCache(cacheFilePath, processedOptions.cacheStrategy) | |
610 | : null; | |
611 | ||
612 | privateMembers.set(this, { | |
613 | options: processedOptions, | |
614 | linter, | |
615 | cacheFilePath, | |
616 | lintResultCache, | |
617 | defaultConfigs, | |
618 | defaultIgnores: () => false, | |
619 | configs: null | |
620 | }); | |
621 | ||
622 | /** | |
623 | * If additional plugins are passed in, add that to the default | |
624 | * configs for this instance. | |
625 | */ | |
626 | if (options.plugins) { | |
627 | ||
628 | const plugins = {}; | |
629 | ||
630 | for (const [pluginName, plugin] of Object.entries(options.plugins)) { | |
631 | plugins[naming.getShorthandName(pluginName, "eslint-plugin")] = plugin; | |
632 | } | |
633 | ||
634 | defaultConfigs.push({ | |
635 | plugins | |
636 | }); | |
637 | } | |
638 | ||
639 | } | |
640 | ||
641 | /** | |
642 | * The version text. | |
643 | * @type {string} | |
644 | */ | |
645 | static get version() { | |
646 | return version; | |
647 | } | |
648 | ||
649 | /** | |
650 | * Outputs fixes from the given results to files. | |
651 | * @param {LintResult[]} results The lint results. | |
652 | * @returns {Promise<void>} Returns a promise that is used to track side effects. | |
653 | */ | |
654 | static async outputFixes(results) { | |
655 | if (!Array.isArray(results)) { | |
656 | throw new Error("'results' must be an array"); | |
657 | } | |
658 | ||
659 | await Promise.all( | |
660 | results | |
661 | .filter(result => { | |
662 | if (typeof result !== "object" || result === null) { | |
663 | throw new Error("'results' must include only objects"); | |
664 | } | |
665 | return ( | |
666 | typeof result.output === "string" && | |
667 | path.isAbsolute(result.filePath) | |
668 | ); | |
669 | }) | |
670 | .map(r => fs.writeFile(r.filePath, r.output)) | |
671 | ); | |
672 | } | |
673 | ||
674 | /** | |
675 | * Returns results that only contains errors. | |
676 | * @param {LintResult[]} results The results to filter. | |
677 | * @returns {LintResult[]} The filtered results. | |
678 | */ | |
679 | static getErrorResults(results) { | |
680 | const filtered = []; | |
681 | ||
682 | results.forEach(result => { | |
683 | const filteredMessages = result.messages.filter(isErrorMessage); | |
684 | const filteredSuppressedMessages = result.suppressedMessages.filter(isErrorMessage); | |
685 | ||
686 | if (filteredMessages.length > 0) { | |
687 | filtered.push({ | |
688 | ...result, | |
689 | messages: filteredMessages, | |
690 | suppressedMessages: filteredSuppressedMessages, | |
691 | errorCount: filteredMessages.length, | |
692 | warningCount: 0, | |
693 | fixableErrorCount: result.fixableErrorCount, | |
694 | fixableWarningCount: 0 | |
695 | }); | |
696 | } | |
697 | }); | |
698 | ||
699 | return filtered; | |
700 | } | |
701 | ||
702 | /** | |
703 | * Returns meta objects for each rule represented in the lint results. | |
704 | * @param {LintResult[]} results The results to fetch rules meta for. | |
705 | * @returns {Object} A mapping of ruleIds to rule meta objects. | |
706 | * @throws {TypeError} When the results object wasn't created from this ESLint instance. | |
707 | * @throws {TypeError} When a plugin or rule is missing. | |
708 | */ | |
709 | getRulesMetaForResults(results) { | |
710 | ||
711 | const resultRules = new Map(); | |
712 | ||
713 | // short-circuit simple case | |
714 | if (results.length === 0) { | |
715 | return resultRules; | |
716 | } | |
717 | ||
718 | const { configs } = privateMembers.get(this); | |
719 | ||
720 | /* | |
721 | * We can only accurately return rules meta information for linting results if the | |
722 | * results were created by this instance. Otherwise, the necessary rules data is | |
723 | * not available. So if the config array doesn't already exist, just throw an error | |
724 | * to let the user know we can't do anything here. | |
725 | */ | |
726 | if (!configs) { | |
727 | throw new TypeError("Results object was not created from this ESLint instance."); | |
728 | } | |
729 | ||
730 | for (const result of results) { | |
731 | ||
732 | /* | |
733 | * Normalize filename for <text>. | |
734 | */ | |
735 | const filePath = result.filePath === "<text>" | |
736 | ? "__placeholder__.js" : result.filePath; | |
737 | ||
738 | /* | |
739 | * All of the plugin and rule information is contained within the | |
740 | * calculated config for the given file. | |
741 | */ | |
742 | const config = configs.getConfig(filePath); | |
743 | const allMessages = result.messages.concat(result.suppressedMessages); | |
744 | ||
745 | for (const { ruleId } of allMessages) { | |
746 | const rule = getRuleFromConfig(ruleId, config); | |
747 | ||
748 | // ensure the rule exists | |
749 | if (!rule) { | |
750 | throw new TypeError(`Could not find the rule "${ruleId}".`); | |
751 | } | |
752 | ||
753 | resultRules.set(ruleId, rule); | |
754 | } | |
755 | } | |
756 | ||
757 | return createRulesMeta(resultRules); | |
758 | } | |
759 | ||
760 | /** | |
761 | * Executes the current configuration on an array of file and directory names. | |
762 | * @param {string|string[]} patterns An array of file and directory names. | |
763 | * @returns {Promise<LintResult[]>} The results of linting the file patterns given. | |
764 | */ | |
765 | async lintFiles(patterns) { | |
766 | if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) { | |
767 | throw new Error("'patterns' must be a non-empty string or an array of non-empty strings"); | |
768 | } | |
769 | ||
770 | const { | |
771 | cacheFilePath, | |
772 | lintResultCache, | |
773 | linter, | |
774 | options: eslintOptions | |
775 | } = privateMembers.get(this); | |
776 | const configs = await calculateConfigArray(this, eslintOptions); | |
777 | const { | |
778 | allowInlineConfig, | |
779 | cache, | |
780 | cwd, | |
781 | fix, | |
782 | fixTypes, | |
783 | reportUnusedDisableDirectives, | |
784 | globInputPaths, | |
785 | errorOnUnmatchedPattern | |
786 | } = eslintOptions; | |
787 | const startTime = Date.now(); | |
788 | const usedConfigs = []; | |
789 | const fixTypesSet = fixTypes ? new Set(fixTypes) : null; | |
790 | ||
791 | // Delete cache file; should this be done here? | |
792 | if (!cache && cacheFilePath) { | |
793 | debug(`Deleting cache file at ${cacheFilePath}`); | |
794 | ||
795 | try { | |
796 | await fs.unlink(cacheFilePath); | |
797 | } catch (error) { | |
798 | const errorCode = error && error.code; | |
799 | ||
800 | // Ignore errors when no such file exists or file system is read only (and cache file does not exist) | |
801 | if (errorCode !== "ENOENT" && !(errorCode === "EROFS" && !(await fs.exists(cacheFilePath)))) { | |
802 | throw error; | |
803 | } | |
804 | } | |
805 | } | |
806 | ||
807 | const filePaths = await findFiles({ | |
808 | patterns: typeof patterns === "string" ? [patterns] : patterns, | |
809 | cwd, | |
810 | globInputPaths, | |
811 | configs, | |
812 | errorOnUnmatchedPattern | |
813 | }); | |
814 | ||
815 | debug(`${filePaths.length} files found in: ${Date.now() - startTime}ms`); | |
816 | ||
817 | /* | |
818 | * Because we need to process multiple files, including reading from disk, | |
819 | * it is most efficient to start by reading each file via promises so that | |
820 | * they can be done in parallel. Then, we can lint the returned text. This | |
821 | * ensures we are waiting the minimum amount of time in between lints. | |
822 | */ | |
823 | const results = await Promise.all( | |
824 | ||
825 | filePaths.map(({ filePath, ignored }) => { | |
826 | ||
827 | /* | |
828 | * If a filename was entered that matches an ignore | |
829 | * pattern, then notify the user. | |
830 | */ | |
831 | if (ignored) { | |
832 | return createIgnoreResult(filePath, cwd); | |
833 | } | |
834 | ||
835 | const config = configs.getConfig(filePath); | |
836 | ||
837 | /* | |
838 | * Sometimes a file found through a glob pattern will | |
839 | * be ignored. In this case, `config` will be undefined | |
840 | * and we just silently ignore the file. | |
841 | */ | |
842 | if (!config) { | |
843 | return void 0; | |
844 | } | |
845 | ||
846 | /* | |
847 | * Store used configs for: | |
848 | * - this method uses to collect used deprecated rules. | |
849 | * - `--fix-type` option uses to get the loaded rule's meta data. | |
850 | */ | |
851 | if (!usedConfigs.includes(config)) { | |
852 | usedConfigs.push(config); | |
853 | } | |
854 | ||
855 | // Skip if there is cached result. | |
856 | if (lintResultCache) { | |
857 | const cachedResult = | |
858 | lintResultCache.getCachedLintResults(filePath, config); | |
859 | ||
860 | if (cachedResult) { | |
861 | const hadMessages = | |
862 | cachedResult.messages && | |
863 | cachedResult.messages.length > 0; | |
864 | ||
865 | if (hadMessages && fix) { | |
866 | debug(`Reprocessing cached file to allow autofix: ${filePath}`); | |
867 | } else { | |
868 | debug(`Skipping file since it hasn't changed: ${filePath}`); | |
869 | return cachedResult; | |
870 | } | |
871 | } | |
872 | } | |
873 | ||
874 | ||
875 | // set up fixer for fixtypes if necessary | |
876 | let fixer = fix; | |
877 | ||
878 | if (fix && fixTypesSet) { | |
879 | ||
880 | // save original value of options.fix in case it's a function | |
881 | const originalFix = (typeof fix === "function") | |
882 | ? fix : () => true; | |
883 | ||
884 | fixer = message => shouldMessageBeFixed(message, config, fixTypesSet) && originalFix(message); | |
885 | } | |
886 | ||
887 | return fs.readFile(filePath, "utf8") | |
888 | .then(text => { | |
889 | ||
890 | // do the linting | |
891 | const result = verifyText({ | |
892 | text, | |
893 | filePath, | |
894 | configs, | |
895 | cwd, | |
896 | fix: fixer, | |
897 | allowInlineConfig, | |
898 | reportUnusedDisableDirectives, | |
899 | linter | |
900 | }); | |
901 | ||
902 | /* | |
903 | * Store the lint result in the LintResultCache. | |
904 | * NOTE: The LintResultCache will remove the file source and any | |
905 | * other properties that are difficult to serialize, and will | |
906 | * hydrate those properties back in on future lint runs. | |
907 | */ | |
908 | if (lintResultCache) { | |
909 | lintResultCache.setCachedLintResults(filePath, config, result); | |
910 | } | |
911 | ||
912 | return result; | |
913 | }); | |
914 | ||
915 | }) | |
916 | ); | |
917 | ||
918 | // Persist the cache to disk. | |
919 | if (lintResultCache) { | |
920 | lintResultCache.reconcile(); | |
921 | } | |
922 | ||
923 | let usedDeprecatedRules; | |
924 | const finalResults = results.filter(result => !!result); | |
925 | ||
926 | return processLintReport(this, { | |
927 | results: finalResults, | |
928 | ...calculateStatsPerRun(finalResults), | |
929 | ||
930 | // Initialize it lazily because CLI and `ESLint` API don't use it. | |
931 | get usedDeprecatedRules() { | |
932 | if (!usedDeprecatedRules) { | |
933 | usedDeprecatedRules = Array.from( | |
934 | iterateRuleDeprecationWarnings(usedConfigs) | |
935 | ); | |
936 | } | |
937 | return usedDeprecatedRules; | |
938 | } | |
939 | }); | |
940 | } | |
941 | ||
942 | /** | |
943 | * Executes the current configuration on text. | |
944 | * @param {string} code A string of JavaScript code to lint. | |
945 | * @param {Object} [options] The options. | |
946 | * @param {string} [options.filePath] The path to the file of the source code. | |
947 | * @param {boolean} [options.warnIgnored] When set to true, warn if given filePath is an ignored path. | |
948 | * @returns {Promise<LintResult[]>} The results of linting the string of code given. | |
949 | */ | |
950 | async lintText(code, options = {}) { | |
951 | ||
952 | // Parameter validation | |
953 | ||
954 | if (typeof code !== "string") { | |
955 | throw new Error("'code' must be a string"); | |
956 | } | |
957 | ||
958 | if (typeof options !== "object") { | |
959 | throw new Error("'options' must be an object, null, or undefined"); | |
960 | } | |
961 | ||
962 | // Options validation | |
963 | ||
964 | const { | |
965 | filePath, | |
966 | warnIgnored = false, | |
967 | ...unknownOptions | |
968 | } = options || {}; | |
969 | ||
970 | const unknownOptionKeys = Object.keys(unknownOptions); | |
971 | ||
972 | if (unknownOptionKeys.length > 0) { | |
973 | throw new Error(`'options' must not include the unknown option(s): ${unknownOptionKeys.join(", ")}`); | |
974 | } | |
975 | ||
976 | if (filePath !== void 0 && !isNonEmptyString(filePath)) { | |
977 | throw new Error("'options.filePath' must be a non-empty string or undefined"); | |
978 | } | |
979 | ||
980 | if (typeof warnIgnored !== "boolean") { | |
981 | throw new Error("'options.warnIgnored' must be a boolean or undefined"); | |
982 | } | |
983 | ||
984 | // Now we can get down to linting | |
985 | ||
986 | const { | |
987 | linter, | |
988 | options: eslintOptions | |
989 | } = privateMembers.get(this); | |
990 | const configs = await calculateConfigArray(this, eslintOptions); | |
991 | const { | |
992 | allowInlineConfig, | |
993 | cwd, | |
994 | fix, | |
995 | reportUnusedDisableDirectives | |
996 | } = eslintOptions; | |
997 | const results = []; | |
998 | const startTime = Date.now(); | |
999 | const resolvedFilename = path.resolve(cwd, filePath || "__placeholder__.js"); | |
1000 | let config; | |
1001 | ||
1002 | // Clear the last used config arrays. | |
1003 | if (resolvedFilename && await this.isPathIgnored(resolvedFilename)) { | |
1004 | if (warnIgnored) { | |
1005 | results.push(createIgnoreResult(resolvedFilename, cwd)); | |
1006 | } | |
1007 | } else { | |
1008 | ||
1009 | // TODO: Needed? | |
1010 | config = configs.getConfig(resolvedFilename); | |
1011 | ||
1012 | // Do lint. | |
1013 | results.push(verifyText({ | |
1014 | text: code, | |
1015 | filePath: resolvedFilename.endsWith("__placeholder__.js") ? "<text>" : resolvedFilename, | |
1016 | configs, | |
1017 | cwd, | |
1018 | fix, | |
1019 | allowInlineConfig, | |
1020 | reportUnusedDisableDirectives, | |
1021 | linter | |
1022 | })); | |
1023 | } | |
1024 | ||
1025 | debug(`Linting complete in: ${Date.now() - startTime}ms`); | |
1026 | let usedDeprecatedRules; | |
1027 | ||
1028 | return processLintReport(this, { | |
1029 | results, | |
1030 | ...calculateStatsPerRun(results), | |
1031 | ||
1032 | // Initialize it lazily because CLI and `ESLint` API don't use it. | |
1033 | get usedDeprecatedRules() { | |
1034 | if (!usedDeprecatedRules) { | |
1035 | usedDeprecatedRules = Array.from( | |
1036 | iterateRuleDeprecationWarnings(config) | |
1037 | ); | |
1038 | } | |
1039 | return usedDeprecatedRules; | |
1040 | } | |
1041 | }); | |
1042 | ||
1043 | } | |
1044 | ||
1045 | /** | |
1046 | * Returns the formatter representing the given formatter name. | |
1047 | * @param {string} [name] The name of the formatter to load. | |
1048 | * The following values are allowed: | |
1049 | * - `undefined` ... Load `stylish` builtin formatter. | |
1050 | * - A builtin formatter name ... Load the builtin formatter. | |
1051 | * - A thirdparty formatter name: | |
1052 | * - `foo` → `eslint-formatter-foo` | |
1053 | * - `@foo` → `@foo/eslint-formatter` | |
1054 | * - `@foo/bar` → `@foo/eslint-formatter-bar` | |
1055 | * - A file path ... Load the file. | |
1056 | * @returns {Promise<Formatter>} A promise resolving to the formatter object. | |
1057 | * This promise will be rejected if the given formatter was not found or not | |
1058 | * a function. | |
1059 | */ | |
1060 | async loadFormatter(name = "stylish") { | |
1061 | if (typeof name !== "string") { | |
1062 | throw new Error("'name' must be a string"); | |
1063 | } | |
1064 | ||
1065 | // replace \ with / for Windows compatibility | |
1066 | const normalizedFormatName = name.replace(/\\/gu, "/"); | |
1067 | const namespace = naming.getNamespaceFromTerm(normalizedFormatName); | |
1068 | ||
1069 | // grab our options | |
1070 | const { cwd } = privateMembers.get(this).options; | |
1071 | ||
1072 | ||
1073 | let formatterPath; | |
1074 | ||
1075 | // if there's a slash, then it's a file (TODO: this check seems dubious for scoped npm packages) | |
1076 | if (!namespace && normalizedFormatName.includes("/")) { | |
1077 | formatterPath = path.resolve(cwd, normalizedFormatName); | |
1078 | } else { | |
1079 | try { | |
1080 | const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter"); | |
1081 | ||
1082 | // TODO: This is pretty dirty...would be nice to clean up at some point. | |
1083 | formatterPath = ModuleResolver.resolve(npmFormat, path.join(cwd, "__placeholder__.js")); | |
1084 | } catch { | |
1085 | formatterPath = path.resolve(__dirname, "../", "cli-engine", "formatters", `${normalizedFormatName}.js`); | |
1086 | } | |
1087 | } | |
1088 | ||
1089 | let formatter; | |
1090 | ||
1091 | try { | |
1092 | formatter = (await import(pathToFileURL(formatterPath))).default; | |
1093 | } catch (ex) { | |
1094 | ||
1095 | // check for formatters that have been removed | |
1096 | if (removedFormatters.has(name)) { | |
1097 | ex.message = `The ${name} formatter is no longer part of core ESLint. Install it manually with \`npm install -D eslint-formatter-${name}\``; | |
1098 | } else { | |
1099 | ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`; | |
1100 | } | |
1101 | ||
1102 | throw ex; | |
1103 | } | |
1104 | ||
1105 | ||
1106 | if (typeof formatter !== "function") { | |
1107 | throw new TypeError(`Formatter must be a function, but got a ${typeof formatter}.`); | |
1108 | } | |
1109 | ||
1110 | const eslint = this; | |
1111 | ||
1112 | return { | |
1113 | ||
1114 | /** | |
1115 | * The main formatter method. | |
1116 | * @param {LintResults[]} results The lint results to format. | |
1117 | * @returns {string} The formatted lint results. | |
1118 | */ | |
1119 | format(results) { | |
1120 | let rulesMeta = null; | |
1121 | ||
1122 | results.sort(compareResultsByFilePath); | |
1123 | ||
1124 | return formatter(results, { | |
1125 | cwd, | |
1126 | get rulesMeta() { | |
1127 | if (!rulesMeta) { | |
1128 | rulesMeta = eslint.getRulesMetaForResults(results); | |
1129 | } | |
1130 | ||
1131 | return rulesMeta; | |
1132 | } | |
1133 | }); | |
1134 | } | |
1135 | }; | |
1136 | } | |
1137 | ||
1138 | /** | |
1139 | * Returns a configuration object for the given file based on the CLI options. | |
1140 | * This is the same logic used by the ESLint CLI executable to determine | |
1141 | * configuration for each file it processes. | |
1142 | * @param {string} filePath The path of the file to retrieve a config object for. | |
1143 | * @returns {Promise<ConfigData|undefined>} A configuration object for the file | |
1144 | * or `undefined` if there is no configuration data for the object. | |
1145 | */ | |
1146 | async calculateConfigForFile(filePath) { | |
1147 | if (!isNonEmptyString(filePath)) { | |
1148 | throw new Error("'filePath' must be a non-empty string"); | |
1149 | } | |
1150 | const options = privateMembers.get(this).options; | |
1151 | const absolutePath = path.resolve(options.cwd, filePath); | |
1152 | const configs = await calculateConfigArray(this, options); | |
1153 | ||
1154 | return configs.getConfig(absolutePath); | |
1155 | } | |
1156 | ||
1157 | /** | |
1158 | * Checks if a given path is ignored by ESLint. | |
1159 | * @param {string} filePath The path of the file to check. | |
1160 | * @returns {Promise<boolean>} Whether or not the given path is ignored. | |
1161 | */ | |
1162 | async isPathIgnored(filePath) { | |
1163 | const config = await this.calculateConfigForFile(filePath); | |
1164 | ||
1165 | return config === void 0; | |
1166 | } | |
1167 | } | |
1168 | ||
1169 | //------------------------------------------------------------------------------ | |
1170 | // Public Interface | |
1171 | //------------------------------------------------------------------------------ | |
1172 | ||
1173 | module.exports = { | |
1174 | FlatESLint, | |
1175 | findFlatConfigFile | |
1176 | }; |