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