]>
Commit | Line | Data |
---|---|---|
56c4a2cb DC |
1 | /** |
2 | * @fileoverview Main API Class | |
3 | * @author Kai Cataldo | |
4 | * @author Toru Nagashima | |
5 | */ | |
6 | ||
7 | "use strict"; | |
8 | ||
9 | //------------------------------------------------------------------------------ | |
10 | // Requirements | |
11 | //------------------------------------------------------------------------------ | |
12 | ||
13 | const path = require("path"); | |
14 | const fs = require("fs"); | |
15 | const { promisify } = require("util"); | |
16 | const { CLIEngine, getCLIEngineInternalSlots } = require("../cli-engine/cli-engine"); | |
17 | const BuiltinRules = require("../rules"); | |
6f036462 TL |
18 | const { |
19 | Legacy: { | |
20 | ConfigOps: { | |
21 | getRuleSeverity | |
22 | } | |
23 | } | |
24 | } = require("@eslint/eslintrc"); | |
56c4a2cb DC |
25 | const { version } = require("../../package.json"); |
26 | ||
27 | //------------------------------------------------------------------------------ | |
28 | // Typedefs | |
29 | //------------------------------------------------------------------------------ | |
30 | ||
31 | /** @typedef {import("../cli-engine/cli-engine").LintReport} CLIEngineLintReport */ | |
32 | /** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ | |
33 | /** @typedef {import("../shared/types").ConfigData} ConfigData */ | |
34 | /** @typedef {import("../shared/types").LintMessage} LintMessage */ | |
8f9d1d4d | 35 | /** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */ |
56c4a2cb DC |
36 | /** @typedef {import("../shared/types").Plugin} Plugin */ |
37 | /** @typedef {import("../shared/types").Rule} Rule */ | |
8f9d1d4d | 38 | /** @typedef {import("../shared/types").LintResult} LintResult */ |
f2a92ac6 | 39 | /** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */ |
34eeec05 TL |
40 | |
41 | /** | |
42 | * The main formatter object. | |
8f9d1d4d | 43 | * @typedef LoadedFormatter |
f2a92ac6 | 44 | * @property {(results: LintResult[], resultsMeta: ResultsMeta) => string | Promise<string>} format format function. |
34eeec05 | 45 | */ |
56c4a2cb DC |
46 | |
47 | /** | |
48 | * The options with which to configure the ESLint instance. | |
49 | * @typedef {Object} ESLintOptions | |
50 | * @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments. | |
51 | * @property {ConfigData} [baseConfig] Base config object, extended by all configs used with this instance | |
52 | * @property {boolean} [cache] Enable result caching. | |
53 | * @property {string} [cacheLocation] The cache file to use instead of .eslintcache. | |
5422a9cc | 54 | * @property {"metadata" | "content"} [cacheStrategy] The strategy used to detect changed files. |
56c4a2cb DC |
55 | * @property {string} [cwd] The value to use for the current working directory. |
56 | * @property {boolean} [errorOnUnmatchedPattern] If `false` then `ESLint#lintFiles()` doesn't throw even if no target files found. Defaults to `true`. | |
57 | * @property {string[]} [extensions] An array of file extensions to check. | |
58 | * @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean. | |
59 | * @property {string[]} [fixTypes] Array of rule types to apply fixes for. | |
60 | * @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. | |
61 | * @property {boolean} [ignore] False disables use of .eslintignore. | |
62 | * @property {string} [ignorePath] The ignore file to use instead of .eslintignore. | |
63 | * @property {ConfigData} [overrideConfig] Override config object, overrides all configs used with this instance | |
64 | * @property {string} [overrideConfigFile] The configuration file to use. | |
609c276f | 65 | * @property {Record<string,Plugin>|null} [plugins] Preloaded plugins. This is a map-like object, keys are plugin IDs and each value is implementation. |
56c4a2cb DC |
66 | * @property {"error" | "warn" | "off"} [reportUnusedDisableDirectives] the severity to report unused eslint-disable directives. |
67 | * @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD. | |
68 | * @property {string[]} [rulePaths] An array of directories to load custom rules from. | |
69 | * @property {boolean} [useEslintrc] False disables looking for .eslintrc.* files. | |
70 | */ | |
71 | ||
72 | /** | |
73 | * A rules metadata object. | |
74 | * @typedef {Object} RulesMeta | |
75 | * @property {string} id The plugin ID. | |
76 | * @property {Object} definition The plugin definition. | |
77 | */ | |
78 | ||
56c4a2cb DC |
79 | /** |
80 | * Private members for the `ESLint` instance. | |
81 | * @typedef {Object} ESLintPrivateMembers | |
82 | * @property {CLIEngine} cliEngine The wrapped CLIEngine instance. | |
83 | * @property {ESLintOptions} options The options used to instantiate the ESLint instance. | |
84 | */ | |
85 | ||
86 | //------------------------------------------------------------------------------ | |
87 | // Helpers | |
88 | //------------------------------------------------------------------------------ | |
89 | ||
90 | const writeFile = promisify(fs.writeFile); | |
91 | ||
92 | /** | |
93 | * The map with which to store private class members. | |
94 | * @type {WeakMap<ESLint, ESLintPrivateMembers>} | |
95 | */ | |
96 | const privateMembersMap = new WeakMap(); | |
97 | ||
98 | /** | |
99 | * Check if a given value is a non-empty string or not. | |
100 | * @param {any} x The value to check. | |
101 | * @returns {boolean} `true` if `x` is a non-empty string. | |
102 | */ | |
103 | function isNonEmptyString(x) { | |
104 | return typeof x === "string" && x.trim() !== ""; | |
105 | } | |
106 | ||
107 | /** | |
8f9d1d4d | 108 | * Check if a given value is an array of non-empty strings or not. |
56c4a2cb | 109 | * @param {any} x The value to check. |
8f9d1d4d | 110 | * @returns {boolean} `true` if `x` is an array of non-empty strings. |
56c4a2cb DC |
111 | */ |
112 | function isArrayOfNonEmptyString(x) { | |
113 | return Array.isArray(x) && x.every(isNonEmptyString); | |
114 | } | |
115 | ||
116 | /** | |
117 | * Check if a given value is a valid fix type or not. | |
118 | * @param {any} x The value to check. | |
119 | * @returns {boolean} `true` if `x` is valid fix type. | |
120 | */ | |
121 | function isFixType(x) { | |
609c276f | 122 | return x === "directive" || x === "problem" || x === "suggestion" || x === "layout"; |
56c4a2cb DC |
123 | } |
124 | ||
125 | /** | |
126 | * Check if a given value is an array of fix types or not. | |
127 | * @param {any} x The value to check. | |
128 | * @returns {boolean} `true` if `x` is an array of fix types. | |
129 | */ | |
130 | function isFixTypeArray(x) { | |
131 | return Array.isArray(x) && x.every(isFixType); | |
132 | } | |
133 | ||
134 | /** | |
135 | * The error for invalid options. | |
136 | */ | |
137 | class ESLintInvalidOptionsError extends Error { | |
138 | constructor(messages) { | |
139 | super(`Invalid Options:\n- ${messages.join("\n- ")}`); | |
140 | this.code = "ESLINT_INVALID_OPTIONS"; | |
141 | Error.captureStackTrace(this, ESLintInvalidOptionsError); | |
142 | } | |
143 | } | |
144 | ||
145 | /** | |
146 | * Validates and normalizes options for the wrapped CLIEngine instance. | |
147 | * @param {ESLintOptions} options The options to process. | |
609c276f | 148 | * @throws {ESLintInvalidOptionsError} If of any of a variety of type errors. |
56c4a2cb DC |
149 | * @returns {ESLintOptions} The normalized options. |
150 | */ | |
151 | function processOptions({ | |
152 | allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instead because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored. | |
153 | baseConfig = null, | |
154 | cache = false, | |
155 | cacheLocation = ".eslintcache", | |
5422a9cc | 156 | cacheStrategy = "metadata", |
56c4a2cb DC |
157 | cwd = process.cwd(), |
158 | errorOnUnmatchedPattern = true, | |
159 | extensions = null, // ← should be null by default because if it's an array then it suppresses RFC20 feature. | |
160 | fix = false, | |
161 | fixTypes = null, // ← should be null by default because if it's an array then it suppresses rules that don't have the `meta.type` property. | |
162 | globInputPaths = true, | |
163 | ignore = true, | |
164 | ignorePath = null, // ← should be null by default because if it's a string then it may throw ENOENT. | |
165 | overrideConfig = null, | |
166 | overrideConfigFile = null, | |
167 | plugins = {}, | |
168 | reportUnusedDisableDirectives = null, // ← should be null by default because if it's a string then it overrides the 'reportUnusedDisableDirectives' setting in config files. And we cannot use `overrideConfig.reportUnusedDisableDirectives` instead because we cannot configure the `error` severity with that. | |
169 | resolvePluginsRelativeTo = null, // ← should be null by default because if it's a string then it suppresses RFC47 feature. | |
170 | rulePaths = [], | |
171 | useEslintrc = true, | |
172 | ...unknownOptions | |
173 | }) { | |
174 | const errors = []; | |
175 | const unknownOptionKeys = Object.keys(unknownOptions); | |
176 | ||
177 | if (unknownOptionKeys.length >= 1) { | |
178 | errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`); | |
179 | if (unknownOptionKeys.includes("cacheFile")) { | |
180 | errors.push("'cacheFile' has been removed. Please use the 'cacheLocation' option instead."); | |
181 | } | |
182 | if (unknownOptionKeys.includes("configFile")) { | |
183 | errors.push("'configFile' has been removed. Please use the 'overrideConfigFile' option instead."); | |
184 | } | |
185 | if (unknownOptionKeys.includes("envs")) { | |
186 | errors.push("'envs' has been removed. Please use the 'overrideConfig.env' option instead."); | |
187 | } | |
188 | if (unknownOptionKeys.includes("globals")) { | |
189 | errors.push("'globals' has been removed. Please use the 'overrideConfig.globals' option instead."); | |
190 | } | |
191 | if (unknownOptionKeys.includes("ignorePattern")) { | |
192 | errors.push("'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead."); | |
193 | } | |
194 | if (unknownOptionKeys.includes("parser")) { | |
195 | errors.push("'parser' has been removed. Please use the 'overrideConfig.parser' option instead."); | |
196 | } | |
197 | if (unknownOptionKeys.includes("parserOptions")) { | |
198 | errors.push("'parserOptions' has been removed. Please use the 'overrideConfig.parserOptions' option instead."); | |
199 | } | |
200 | if (unknownOptionKeys.includes("rules")) { | |
201 | errors.push("'rules' has been removed. Please use the 'overrideConfig.rules' option instead."); | |
202 | } | |
203 | } | |
204 | if (typeof allowInlineConfig !== "boolean") { | |
205 | errors.push("'allowInlineConfig' must be a boolean."); | |
206 | } | |
207 | if (typeof baseConfig !== "object") { | |
208 | errors.push("'baseConfig' must be an object or null."); | |
209 | } | |
210 | if (typeof cache !== "boolean") { | |
211 | errors.push("'cache' must be a boolean."); | |
212 | } | |
213 | if (!isNonEmptyString(cacheLocation)) { | |
214 | errors.push("'cacheLocation' must be a non-empty string."); | |
215 | } | |
5422a9cc TL |
216 | if ( |
217 | cacheStrategy !== "metadata" && | |
218 | cacheStrategy !== "content" | |
219 | ) { | |
220 | errors.push("'cacheStrategy' must be any of \"metadata\", \"content\"."); | |
221 | } | |
56c4a2cb DC |
222 | if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) { |
223 | errors.push("'cwd' must be an absolute path."); | |
224 | } | |
225 | if (typeof errorOnUnmatchedPattern !== "boolean") { | |
226 | errors.push("'errorOnUnmatchedPattern' must be a boolean."); | |
227 | } | |
228 | if (!isArrayOfNonEmptyString(extensions) && extensions !== null) { | |
229 | errors.push("'extensions' must be an array of non-empty strings or null."); | |
230 | } | |
231 | if (typeof fix !== "boolean" && typeof fix !== "function") { | |
232 | errors.push("'fix' must be a boolean or a function."); | |
233 | } | |
234 | if (fixTypes !== null && !isFixTypeArray(fixTypes)) { | |
609c276f | 235 | errors.push("'fixTypes' must be an array of any of \"directive\", \"problem\", \"suggestion\", and \"layout\"."); |
56c4a2cb DC |
236 | } |
237 | if (typeof globInputPaths !== "boolean") { | |
238 | errors.push("'globInputPaths' must be a boolean."); | |
239 | } | |
240 | if (typeof ignore !== "boolean") { | |
241 | errors.push("'ignore' must be a boolean."); | |
242 | } | |
243 | if (!isNonEmptyString(ignorePath) && ignorePath !== null) { | |
244 | errors.push("'ignorePath' must be a non-empty string or null."); | |
245 | } | |
246 | if (typeof overrideConfig !== "object") { | |
247 | errors.push("'overrideConfig' must be an object or null."); | |
248 | } | |
249 | if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null) { | |
250 | errors.push("'overrideConfigFile' must be a non-empty string or null."); | |
251 | } | |
252 | if (typeof plugins !== "object") { | |
253 | errors.push("'plugins' must be an object or null."); | |
254 | } else if (plugins !== null && Object.keys(plugins).includes("")) { | |
255 | errors.push("'plugins' must not include an empty string."); | |
256 | } | |
257 | if (Array.isArray(plugins)) { | |
258 | errors.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead."); | |
259 | } | |
260 | if ( | |
261 | reportUnusedDisableDirectives !== "error" && | |
262 | reportUnusedDisableDirectives !== "warn" && | |
263 | reportUnusedDisableDirectives !== "off" && | |
264 | reportUnusedDisableDirectives !== null | |
265 | ) { | |
266 | errors.push("'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null."); | |
267 | } | |
268 | if ( | |
269 | !isNonEmptyString(resolvePluginsRelativeTo) && | |
270 | resolvePluginsRelativeTo !== null | |
271 | ) { | |
272 | errors.push("'resolvePluginsRelativeTo' must be a non-empty string or null."); | |
273 | } | |
274 | if (!isArrayOfNonEmptyString(rulePaths)) { | |
275 | errors.push("'rulePaths' must be an array of non-empty strings."); | |
276 | } | |
277 | if (typeof useEslintrc !== "boolean") { | |
456be15e | 278 | errors.push("'useEslintrc' must be a boolean."); |
56c4a2cb DC |
279 | } |
280 | ||
281 | if (errors.length > 0) { | |
282 | throw new ESLintInvalidOptionsError(errors); | |
283 | } | |
284 | ||
285 | return { | |
286 | allowInlineConfig, | |
287 | baseConfig, | |
288 | cache, | |
289 | cacheLocation, | |
5422a9cc | 290 | cacheStrategy, |
56c4a2cb DC |
291 | configFile: overrideConfigFile, |
292 | cwd, | |
293 | errorOnUnmatchedPattern, | |
294 | extensions, | |
295 | fix, | |
296 | fixTypes, | |
297 | globInputPaths, | |
298 | ignore, | |
299 | ignorePath, | |
300 | reportUnusedDisableDirectives, | |
301 | resolvePluginsRelativeTo, | |
302 | rulePaths, | |
303 | useEslintrc | |
304 | }; | |
305 | } | |
306 | ||
307 | /** | |
308 | * Check if a value has one or more properties and that value is not undefined. | |
309 | * @param {any} obj The value to check. | |
310 | * @returns {boolean} `true` if `obj` has one or more properties that that value is not undefined. | |
311 | */ | |
312 | function hasDefinedProperty(obj) { | |
313 | if (typeof obj === "object" && obj !== null) { | |
314 | for (const key in obj) { | |
315 | if (typeof obj[key] !== "undefined") { | |
316 | return true; | |
317 | } | |
318 | } | |
319 | } | |
320 | return false; | |
321 | } | |
322 | ||
323 | /** | |
324 | * Create rulesMeta object. | |
325 | * @param {Map<string,Rule>} rules a map of rules from which to generate the object. | |
326 | * @returns {Object} metadata for all enabled rules. | |
327 | */ | |
328 | function createRulesMeta(rules) { | |
329 | return Array.from(rules).reduce((retVal, [id, rule]) => { | |
330 | retVal[id] = rule.meta; | |
331 | return retVal; | |
332 | }, {}); | |
333 | } | |
334 | ||
335 | /** @type {WeakMap<ExtractedConfig, DeprecatedRuleInfo[]>} */ | |
336 | const usedDeprecatedRulesCache = new WeakMap(); | |
337 | ||
338 | /** | |
339 | * Create used deprecated rule list. | |
340 | * @param {CLIEngine} cliEngine The CLIEngine instance. | |
341 | * @param {string} maybeFilePath The absolute path to a lint target file or `"<text>"`. | |
342 | * @returns {DeprecatedRuleInfo[]} The used deprecated rule list. | |
343 | */ | |
344 | function getOrFindUsedDeprecatedRules(cliEngine, maybeFilePath) { | |
345 | const { | |
346 | configArrayFactory, | |
347 | options: { cwd } | |
348 | } = getCLIEngineInternalSlots(cliEngine); | |
349 | const filePath = path.isAbsolute(maybeFilePath) | |
350 | ? maybeFilePath | |
351 | : path.join(cwd, "__placeholder__.js"); | |
352 | const configArray = configArrayFactory.getConfigArrayForFile(filePath); | |
353 | const config = configArray.extractConfig(filePath); | |
354 | ||
355 | // Most files use the same config, so cache it. | |
356 | if (!usedDeprecatedRulesCache.has(config)) { | |
357 | const pluginRules = configArray.pluginRules; | |
358 | const retv = []; | |
359 | ||
360 | for (const [ruleId, ruleConf] of Object.entries(config.rules)) { | |
361 | if (getRuleSeverity(ruleConf) === 0) { | |
362 | continue; | |
363 | } | |
364 | const rule = pluginRules.get(ruleId) || BuiltinRules.get(ruleId); | |
365 | const meta = rule && rule.meta; | |
366 | ||
367 | if (meta && meta.deprecated) { | |
368 | retv.push({ ruleId, replacedBy: meta.replacedBy || [] }); | |
369 | } | |
370 | } | |
371 | ||
372 | usedDeprecatedRulesCache.set(config, Object.freeze(retv)); | |
373 | } | |
374 | ||
375 | return usedDeprecatedRulesCache.get(config); | |
376 | } | |
377 | ||
378 | /** | |
379 | * Processes the linting results generated by a CLIEngine linting report to | |
380 | * match the ESLint class's API. | |
381 | * @param {CLIEngine} cliEngine The CLIEngine instance. | |
382 | * @param {CLIEngineLintReport} report The CLIEngine linting report to process. | |
383 | * @returns {LintResult[]} The processed linting results. | |
384 | */ | |
385 | function processCLIEngineLintReport(cliEngine, { results }) { | |
386 | const descriptor = { | |
387 | configurable: true, | |
388 | enumerable: true, | |
389 | get() { | |
390 | return getOrFindUsedDeprecatedRules(cliEngine, this.filePath); | |
391 | } | |
392 | }; | |
393 | ||
394 | for (const result of results) { | |
395 | Object.defineProperty(result, "usedDeprecatedRules", descriptor); | |
396 | } | |
397 | ||
398 | return results; | |
399 | } | |
400 | ||
401 | /** | |
402 | * An Array.prototype.sort() compatible compare function to order results by their file path. | |
403 | * @param {LintResult} a The first lint result. | |
404 | * @param {LintResult} b The second lint result. | |
405 | * @returns {number} An integer representing the order in which the two results should occur. | |
406 | */ | |
407 | function compareResultsByFilePath(a, b) { | |
408 | if (a.filePath < b.filePath) { | |
409 | return -1; | |
410 | } | |
411 | ||
412 | if (a.filePath > b.filePath) { | |
413 | return 1; | |
414 | } | |
415 | ||
416 | return 0; | |
417 | } | |
418 | ||
609c276f TL |
419 | /** |
420 | * Main API. | |
421 | */ | |
56c4a2cb DC |
422 | class ESLint { |
423 | ||
424 | /** | |
425 | * Creates a new instance of the main ESLint API. | |
426 | * @param {ESLintOptions} options The options for this instance. | |
427 | */ | |
428 | constructor(options = {}) { | |
429 | const processedOptions = processOptions(options); | |
609c276f | 430 | const cliEngine = new CLIEngine(processedOptions, { preloadedPlugins: options.plugins }); |
56c4a2cb | 431 | const { |
56c4a2cb DC |
432 | configArrayFactory, |
433 | lastConfigArrays | |
434 | } = getCLIEngineInternalSlots(cliEngine); | |
435 | let updated = false; | |
436 | ||
56c4a2cb DC |
437 | /* |
438 | * Address `overrideConfig` to set override config. | |
439 | * Operate the `configArrayFactory` internal slot directly because this | |
440 | * functionality doesn't exist as the public API of CLIEngine. | |
441 | */ | |
442 | if (hasDefinedProperty(options.overrideConfig)) { | |
443 | configArrayFactory.setOverrideConfig(options.overrideConfig); | |
444 | updated = true; | |
445 | } | |
446 | ||
447 | // Update caches. | |
448 | if (updated) { | |
449 | configArrayFactory.clearCache(); | |
450 | lastConfigArrays[0] = configArrayFactory.getConfigArrayForFile(); | |
451 | } | |
452 | ||
453 | // Initialize private properties. | |
454 | privateMembersMap.set(this, { | |
455 | cliEngine, | |
456 | options: processedOptions | |
457 | }); | |
458 | } | |
459 | ||
460 | /** | |
461 | * The version text. | |
462 | * @type {string} | |
463 | */ | |
464 | static get version() { | |
465 | return version; | |
466 | } | |
467 | ||
468 | /** | |
469 | * Outputs fixes from the given results to files. | |
470 | * @param {LintResult[]} results The lint results. | |
471 | * @returns {Promise<void>} Returns a promise that is used to track side effects. | |
472 | */ | |
473 | static async outputFixes(results) { | |
474 | if (!Array.isArray(results)) { | |
475 | throw new Error("'results' must be an array"); | |
476 | } | |
477 | ||
478 | await Promise.all( | |
479 | results | |
480 | .filter(result => { | |
481 | if (typeof result !== "object" || result === null) { | |
482 | throw new Error("'results' must include only objects"); | |
483 | } | |
484 | return ( | |
485 | typeof result.output === "string" && | |
486 | path.isAbsolute(result.filePath) | |
487 | ); | |
488 | }) | |
489 | .map(r => writeFile(r.filePath, r.output)) | |
490 | ); | |
491 | } | |
492 | ||
493 | /** | |
494 | * Returns results that only contains errors. | |
495 | * @param {LintResult[]} results The results to filter. | |
496 | * @returns {LintResult[]} The filtered results. | |
497 | */ | |
498 | static getErrorResults(results) { | |
499 | return CLIEngine.getErrorResults(results); | |
500 | } | |
501 | ||
609c276f TL |
502 | /** |
503 | * Returns meta objects for each rule represented in the lint results. | |
504 | * @param {LintResult[]} results The results to fetch rules meta for. | |
505 | * @returns {Object} A mapping of ruleIds to rule meta objects. | |
506 | */ | |
507 | getRulesMetaForResults(results) { | |
508 | ||
509 | const resultRuleIds = new Set(); | |
510 | ||
511 | // first gather all ruleIds from all results | |
512 | ||
513 | for (const result of results) { | |
514 | for (const { ruleId } of result.messages) { | |
515 | resultRuleIds.add(ruleId); | |
516 | } | |
8f9d1d4d DC |
517 | for (const { ruleId } of result.suppressedMessages) { |
518 | resultRuleIds.add(ruleId); | |
519 | } | |
609c276f TL |
520 | } |
521 | ||
522 | // create a map of all rules in the results | |
523 | ||
524 | const { cliEngine } = privateMembersMap.get(this); | |
525 | const rules = cliEngine.getRules(); | |
526 | const resultRules = new Map(); | |
527 | ||
528 | for (const [ruleId, rule] of rules) { | |
529 | if (resultRuleIds.has(ruleId)) { | |
530 | resultRules.set(ruleId, rule); | |
531 | } | |
532 | } | |
533 | ||
534 | return createRulesMeta(resultRules); | |
535 | ||
536 | } | |
537 | ||
56c4a2cb DC |
538 | /** |
539 | * Executes the current configuration on an array of file and directory names. | |
540 | * @param {string[]} patterns An array of file and directory names. | |
541 | * @returns {Promise<LintResult[]>} The results of linting the file patterns given. | |
542 | */ | |
543 | async lintFiles(patterns) { | |
544 | if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) { | |
545 | throw new Error("'patterns' must be a non-empty string or an array of non-empty strings"); | |
546 | } | |
547 | const { cliEngine } = privateMembersMap.get(this); | |
548 | ||
549 | return processCLIEngineLintReport( | |
550 | cliEngine, | |
551 | cliEngine.executeOnFiles(patterns) | |
552 | ); | |
553 | } | |
554 | ||
555 | /** | |
556 | * Executes the current configuration on text. | |
557 | * @param {string} code A string of JavaScript code to lint. | |
558 | * @param {Object} [options] The options. | |
559 | * @param {string} [options.filePath] The path to the file of the source code. | |
560 | * @param {boolean} [options.warnIgnored] When set to true, warn if given filePath is an ignored path. | |
561 | * @returns {Promise<LintResult[]>} The results of linting the string of code given. | |
562 | */ | |
563 | async lintText(code, options = {}) { | |
564 | if (typeof code !== "string") { | |
565 | throw new Error("'code' must be a string"); | |
566 | } | |
567 | if (typeof options !== "object") { | |
568 | throw new Error("'options' must be an object, null, or undefined"); | |
569 | } | |
570 | const { | |
571 | filePath, | |
572 | warnIgnored = false, | |
573 | ...unknownOptions | |
574 | } = options || {}; | |
575 | ||
609c276f TL |
576 | const unknownOptionKeys = Object.keys(unknownOptions); |
577 | ||
578 | if (unknownOptionKeys.length > 0) { | |
579 | throw new Error(`'options' must not include the unknown option(s): ${unknownOptionKeys.join(", ")}`); | |
56c4a2cb | 580 | } |
609c276f | 581 | |
56c4a2cb DC |
582 | if (filePath !== void 0 && !isNonEmptyString(filePath)) { |
583 | throw new Error("'options.filePath' must be a non-empty string or undefined"); | |
584 | } | |
585 | if (typeof warnIgnored !== "boolean") { | |
586 | throw new Error("'options.warnIgnored' must be a boolean or undefined"); | |
587 | } | |
588 | ||
589 | const { cliEngine } = privateMembersMap.get(this); | |
590 | ||
591 | return processCLIEngineLintReport( | |
592 | cliEngine, | |
593 | cliEngine.executeOnText(code, filePath, warnIgnored) | |
594 | ); | |
595 | } | |
596 | ||
597 | /** | |
598 | * Returns the formatter representing the given formatter name. | |
456be15e | 599 | * @param {string} [name] The name of the formatter to load. |
56c4a2cb DC |
600 | * The following values are allowed: |
601 | * - `undefined` ... Load `stylish` builtin formatter. | |
602 | * - A builtin formatter name ... Load the builtin formatter. | |
8f9d1d4d | 603 | * - A third-party formatter name: |
56c4a2cb DC |
604 | * - `foo` → `eslint-formatter-foo` |
605 | * - `@foo` → `@foo/eslint-formatter` | |
606 | * - `@foo/bar` → `@foo/eslint-formatter-bar` | |
607 | * - A file path ... Load the file. | |
8f9d1d4d | 608 | * @returns {Promise<LoadedFormatter>} A promise resolving to the formatter object. |
56c4a2cb DC |
609 | * This promise will be rejected if the given formatter was not found or not |
610 | * a function. | |
611 | */ | |
612 | async loadFormatter(name = "stylish") { | |
613 | if (typeof name !== "string") { | |
614 | throw new Error("'name' must be a string"); | |
615 | } | |
616 | ||
34eeec05 | 617 | const { cliEngine, options } = privateMembersMap.get(this); |
56c4a2cb DC |
618 | const formatter = cliEngine.getFormatter(name); |
619 | ||
620 | if (typeof formatter !== "function") { | |
621 | throw new Error(`Formatter must be a function, but got a ${typeof formatter}.`); | |
622 | } | |
623 | ||
624 | return { | |
625 | ||
626 | /** | |
627 | * The main formatter method. | |
8f9d1d4d | 628 | * @param {LintResult[]} results The lint results to format. |
f2a92ac6 | 629 | * @param {ResultsMeta} resultsMeta Warning count and max threshold. |
34eeec05 | 630 | * @returns {string | Promise<string>} The formatted lint results. |
56c4a2cb | 631 | */ |
f2a92ac6 | 632 | format(results, resultsMeta) { |
56c4a2cb DC |
633 | let rulesMeta = null; |
634 | ||
635 | results.sort(compareResultsByFilePath); | |
636 | ||
637 | return formatter(results, { | |
f2a92ac6 | 638 | ...resultsMeta, |
34eeec05 TL |
639 | get cwd() { |
640 | return options.cwd; | |
641 | }, | |
56c4a2cb DC |
642 | get rulesMeta() { |
643 | if (!rulesMeta) { | |
644 | rulesMeta = createRulesMeta(cliEngine.getRules()); | |
645 | } | |
646 | ||
647 | return rulesMeta; | |
648 | } | |
649 | }); | |
650 | } | |
651 | }; | |
652 | } | |
653 | ||
654 | /** | |
655 | * Returns a configuration object for the given file based on the CLI options. | |
656 | * This is the same logic used by the ESLint CLI executable to determine | |
657 | * configuration for each file it processes. | |
658 | * @param {string} filePath The path of the file to retrieve a config object for. | |
659 | * @returns {Promise<ConfigData>} A configuration object for the file. | |
660 | */ | |
661 | async calculateConfigForFile(filePath) { | |
662 | if (!isNonEmptyString(filePath)) { | |
663 | throw new Error("'filePath' must be a non-empty string"); | |
664 | } | |
665 | const { cliEngine } = privateMembersMap.get(this); | |
666 | ||
667 | return cliEngine.getConfigForFile(filePath); | |
668 | } | |
669 | ||
670 | /** | |
671 | * Checks if a given path is ignored by ESLint. | |
672 | * @param {string} filePath The path of the file to check. | |
673 | * @returns {Promise<boolean>} Whether or not the given path is ignored. | |
674 | */ | |
675 | async isPathIgnored(filePath) { | |
676 | if (!isNonEmptyString(filePath)) { | |
677 | throw new Error("'filePath' must be a non-empty string"); | |
678 | } | |
679 | const { cliEngine } = privateMembersMap.get(this); | |
680 | ||
681 | return cliEngine.isPathIgnored(filePath); | |
682 | } | |
683 | } | |
684 | ||
685 | //------------------------------------------------------------------------------ | |
686 | // Public Interface | |
687 | //------------------------------------------------------------------------------ | |
688 | ||
689 | module.exports = { | |
690 | ESLint, | |
691 | ||
692 | /** | |
693 | * Get the private class members of a given ESLint instance for tests. | |
694 | * @param {ESLint} instance The ESLint instance to get. | |
695 | * @returns {ESLintPrivateMembers} The instance's private class members. | |
696 | */ | |
697 | getESLintPrivateMembers(instance) { | |
698 | return privateMembersMap.get(instance); | |
699 | } | |
700 | }; |