2 * @fileoverview Main API Class
4 * @author Toru Nagashima
9 //------------------------------------------------------------------------------
11 //------------------------------------------------------------------------------
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");
24 } = require("@eslint/eslintrc");
25 const { version
} = require("../../package.json");
27 //------------------------------------------------------------------------------
29 //------------------------------------------------------------------------------
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").SuppressedLintMessage} SuppressedLintMessage */
36 /** @typedef {import("../shared/types").Plugin} Plugin */
37 /** @typedef {import("../shared/types").Rule} Rule */
38 /** @typedef {import("../shared/types").LintResult} LintResult */
39 /** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */
42 * The main formatter object.
43 * @typedef LoadedFormatter
44 * @property {(results: LintResult[], resultsMeta: ResultsMeta) => string | Promise<string>} format format function.
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.
54 * @property {"metadata" | "content"} [cacheStrategy] The strategy used to detect changed files.
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.
65 * @property {Record<string,Plugin>|null} [plugins] Preloaded plugins. This is a map-like object, keys are plugin IDs and each value is implementation.
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.
73 * A rules metadata object.
74 * @typedef {Object} RulesMeta
75 * @property {string} id The plugin ID.
76 * @property {Object} definition The plugin definition.
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.
86 //------------------------------------------------------------------------------
88 //------------------------------------------------------------------------------
90 const writeFile
= promisify(fs
.writeFile
);
93 * The map with which to store private class members.
94 * @type {WeakMap<ESLint, ESLintPrivateMembers>}
96 const privateMembersMap
= new WeakMap();
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.
103 function isNonEmptyString(x
) {
104 return typeof x
=== "string" && x
.trim() !== "";
108 * Check if a given value is an array of non-empty strings or not.
109 * @param {any} x The value to check.
110 * @returns {boolean} `true` if `x` is an array of non-empty strings.
112 function isArrayOfNonEmptyString(x
) {
113 return Array
.isArray(x
) && x
.every(isNonEmptyString
);
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.
121 function isFixType(x
) {
122 return x
=== "directive" || x
=== "problem" || x
=== "suggestion" || x
=== "layout";
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.
130 function isFixTypeArray(x
) {
131 return Array
.isArray(x
) && x
.every(isFixType
);
135 * The error for invalid options.
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
);
146 * Validates and normalizes options for the wrapped CLIEngine instance.
147 * @param {ESLintOptions} options The options to process.
148 * @throws {ESLintInvalidOptionsError} If of any of a variety of type errors.
149 * @returns {ESLintOptions} The normalized options.
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.
155 cacheLocation
= ".eslintcache",
156 cacheStrategy
= "metadata",
158 errorOnUnmatchedPattern
= true,
159 extensions
= null, // ← should be null by default because if it's an array then it suppresses RFC20 feature.
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,
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,
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.
175 const unknownOptionKeys
= Object
.keys(unknownOptions
);
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.");
182 if (unknownOptionKeys
.includes("configFile")) {
183 errors
.push("'configFile' has been removed. Please use the 'overrideConfigFile' option instead.");
185 if (unknownOptionKeys
.includes("envs")) {
186 errors
.push("'envs' has been removed. Please use the 'overrideConfig.env' option instead.");
188 if (unknownOptionKeys
.includes("globals")) {
189 errors
.push("'globals' has been removed. Please use the 'overrideConfig.globals' option instead.");
191 if (unknownOptionKeys
.includes("ignorePattern")) {
192 errors
.push("'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead.");
194 if (unknownOptionKeys
.includes("parser")) {
195 errors
.push("'parser' has been removed. Please use the 'overrideConfig.parser' option instead.");
197 if (unknownOptionKeys
.includes("parserOptions")) {
198 errors
.push("'parserOptions' has been removed. Please use the 'overrideConfig.parserOptions' option instead.");
200 if (unknownOptionKeys
.includes("rules")) {
201 errors
.push("'rules' has been removed. Please use the 'overrideConfig.rules' option instead.");
204 if (typeof allowInlineConfig
!== "boolean") {
205 errors
.push("'allowInlineConfig' must be a boolean.");
207 if (typeof baseConfig
!== "object") {
208 errors
.push("'baseConfig' must be an object or null.");
210 if (typeof cache
!== "boolean") {
211 errors
.push("'cache' must be a boolean.");
213 if (!isNonEmptyString(cacheLocation
)) {
214 errors
.push("'cacheLocation' must be a non-empty string.");
217 cacheStrategy
!== "metadata" &&
218 cacheStrategy
!== "content"
220 errors
.push("'cacheStrategy' must be any of \"metadata\", \"content\".");
222 if (!isNonEmptyString(cwd
) || !path
.isAbsolute(cwd
)) {
223 errors
.push("'cwd' must be an absolute path.");
225 if (typeof errorOnUnmatchedPattern
!== "boolean") {
226 errors
.push("'errorOnUnmatchedPattern' must be a boolean.");
228 if (!isArrayOfNonEmptyString(extensions
) && extensions
!== null) {
229 errors
.push("'extensions' must be an array of non-empty strings or null.");
231 if (typeof fix
!== "boolean" && typeof fix
!== "function") {
232 errors
.push("'fix' must be a boolean or a function.");
234 if (fixTypes
!== null && !isFixTypeArray(fixTypes
)) {
235 errors
.push("'fixTypes' must be an array of any of \"directive\", \"problem\", \"suggestion\", and \"layout\".");
237 if (typeof globInputPaths
!== "boolean") {
238 errors
.push("'globInputPaths' must be a boolean.");
240 if (typeof ignore
!== "boolean") {
241 errors
.push("'ignore' must be a boolean.");
243 if (!isNonEmptyString(ignorePath
) && ignorePath
!== null) {
244 errors
.push("'ignorePath' must be a non-empty string or null.");
246 if (typeof overrideConfig
!== "object") {
247 errors
.push("'overrideConfig' must be an object or null.");
249 if (!isNonEmptyString(overrideConfigFile
) && overrideConfigFile
!== null) {
250 errors
.push("'overrideConfigFile' must be a non-empty string or null.");
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.");
257 if (Array
.isArray(plugins
)) {
258 errors
.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead.");
261 reportUnusedDisableDirectives
!== "error" &&
262 reportUnusedDisableDirectives
!== "warn" &&
263 reportUnusedDisableDirectives
!== "off" &&
264 reportUnusedDisableDirectives
!== null
266 errors
.push("'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null.");
269 !isNonEmptyString(resolvePluginsRelativeTo
) &&
270 resolvePluginsRelativeTo
!== null
272 errors
.push("'resolvePluginsRelativeTo' must be a non-empty string or null.");
274 if (!isArrayOfNonEmptyString(rulePaths
)) {
275 errors
.push("'rulePaths' must be an array of non-empty strings.");
277 if (typeof useEslintrc
!== "boolean") {
278 errors
.push("'useEslintrc' must be a boolean.");
281 if (errors
.length
> 0) {
282 throw new ESLintInvalidOptionsError(errors
);
291 configFile
: overrideConfigFile
,
293 errorOnUnmatchedPattern
,
300 reportUnusedDisableDirectives
,
301 resolvePluginsRelativeTo
,
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.
312 function hasDefinedProperty(obj
) {
313 if (typeof obj
=== "object" && obj
!== null) {
314 for (const key
in obj
) {
315 if (typeof obj
[key
] !== "undefined") {
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.
328 function createRulesMeta(rules
) {
329 return Array
.from(rules
).reduce((retVal
, [id
, rule
]) => {
330 retVal
[id
] = rule
.meta
;
335 /** @type {WeakMap<ExtractedConfig, DeprecatedRuleInfo[]>} */
336 const usedDeprecatedRulesCache
= new WeakMap();
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.
344 function getOrFindUsedDeprecatedRules(cliEngine
, maybeFilePath
) {
348 } = getCLIEngineInternalSlots(cliEngine
);
349 const filePath
= path
.isAbsolute(maybeFilePath
)
351 : path
.join(cwd
, "__placeholder__.js");
352 const configArray
= configArrayFactory
.getConfigArrayForFile(filePath
);
353 const config
= configArray
.extractConfig(filePath
);
355 // Most files use the same config, so cache it.
356 if (!usedDeprecatedRulesCache
.has(config
)) {
357 const pluginRules
= configArray
.pluginRules
;
360 for (const [ruleId
, ruleConf
] of Object
.entries(config
.rules
)) {
361 if (getRuleSeverity(ruleConf
) === 0) {
364 const rule
= pluginRules
.get(ruleId
) || BuiltinRules
.get(ruleId
);
365 const meta
= rule
&& rule
.meta
;
367 if (meta
&& meta
.deprecated
) {
368 retv
.push({ ruleId
, replacedBy
: meta
.replacedBy
|| [] });
372 usedDeprecatedRulesCache
.set(config
, Object
.freeze(retv
));
375 return usedDeprecatedRulesCache
.get(config
);
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.
385 function processCLIEngineLintReport(cliEngine
, { results
}) {
390 return getOrFindUsedDeprecatedRules(cliEngine
, this.filePath
);
394 for (const result
of results
) {
395 Object
.defineProperty(result
, "usedDeprecatedRules", descriptor
);
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.
407 function compareResultsByFilePath(a
, b
) {
408 if (a
.filePath
< b
.filePath
) {
412 if (a
.filePath
> b
.filePath
) {
425 * Creates a new instance of the main ESLint API.
426 * @param {ESLintOptions} options The options for this instance.
428 constructor(options
= {}) {
429 const processedOptions
= processOptions(options
);
430 const cliEngine
= new CLIEngine(processedOptions
, { preloadedPlugins
: options
.plugins
});
434 } = getCLIEngineInternalSlots(cliEngine
);
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.
442 if (hasDefinedProperty(options
.overrideConfig
)) {
443 configArrayFactory
.setOverrideConfig(options
.overrideConfig
);
449 configArrayFactory
.clearCache();
450 lastConfigArrays
[0] = configArrayFactory
.getConfigArrayForFile();
453 // Initialize private properties.
454 privateMembersMap
.set(this, {
456 options
: processedOptions
464 static get version() {
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.
473 static async
outputFixes(results
) {
474 if (!Array
.isArray(results
)) {
475 throw new Error("'results' must be an array");
481 if (typeof result
!== "object" || result
=== null) {
482 throw new Error("'results' must include only objects");
485 typeof result
.output
=== "string" &&
486 path
.isAbsolute(result
.filePath
)
489 .map(r
=> writeFile(r
.filePath
, r
.output
))
494 * Returns results that only contains errors.
495 * @param {LintResult[]} results The results to filter.
496 * @returns {LintResult[]} The filtered results.
498 static getErrorResults(results
) {
499 return CLIEngine
.getErrorResults(results
);
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.
507 getRulesMetaForResults(results
) {
509 const resultRuleIds
= new Set();
511 // first gather all ruleIds from all results
513 for (const result
of results
) {
514 for (const { ruleId
} of result
.messages
) {
515 resultRuleIds
.add(ruleId
);
517 for (const { ruleId
} of result
.suppressedMessages
) {
518 resultRuleIds
.add(ruleId
);
522 // create a map of all rules in the results
524 const { cliEngine
} = privateMembersMap
.get(this);
525 const rules
= cliEngine
.getRules();
526 const resultRules
= new Map();
528 for (const [ruleId
, rule
] of rules
) {
529 if (resultRuleIds
.has(ruleId
)) {
530 resultRules
.set(ruleId
, rule
);
534 return createRulesMeta(resultRules
);
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.
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");
547 const { cliEngine
} = privateMembersMap
.get(this);
549 return processCLIEngineLintReport(
551 cliEngine
.executeOnFiles(patterns
)
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.
563 async
lintText(code
, options
= {}) {
564 if (typeof code
!== "string") {
565 throw new Error("'code' must be a string");
567 if (typeof options
!== "object") {
568 throw new Error("'options' must be an object, null, or undefined");
576 const unknownOptionKeys
= Object
.keys(unknownOptions
);
578 if (unknownOptionKeys
.length
> 0) {
579 throw new Error(`'options' must not include the unknown option(s): ${unknownOptionKeys.join(", ")}`);
582 if (filePath
!== void 0 && !isNonEmptyString(filePath
)) {
583 throw new Error("'options.filePath' must be a non-empty string or undefined");
585 if (typeof warnIgnored
!== "boolean") {
586 throw new Error("'options.warnIgnored' must be a boolean or undefined");
589 const { cliEngine
} = privateMembersMap
.get(this);
591 return processCLIEngineLintReport(
593 cliEngine
.executeOnText(code
, filePath
, warnIgnored
)
598 * Returns the formatter representing the given formatter name.
599 * @param {string} [name] The name of the formatter to load.
600 * The following values are allowed:
601 * - `undefined` ... Load `stylish` builtin formatter.
602 * - A builtin formatter name ... Load the builtin formatter.
603 * - A third-party formatter name:
604 * - `foo` → `eslint-formatter-foo`
605 * - `@foo` → `@foo/eslint-formatter`
606 * - `@foo/bar` → `@foo/eslint-formatter-bar`
607 * - A file path ... Load the file.
608 * @returns {Promise<LoadedFormatter>} A promise resolving to the formatter object.
609 * This promise will be rejected if the given formatter was not found or not
612 async
loadFormatter(name
= "stylish") {
613 if (typeof name
!== "string") {
614 throw new Error("'name' must be a string");
617 const { cliEngine
, options
} = privateMembersMap
.get(this);
618 const formatter
= cliEngine
.getFormatter(name
);
620 if (typeof formatter
!== "function") {
621 throw new Error(`Formatter must be a function, but got a ${typeof formatter}.`);
627 * The main formatter method.
628 * @param {LintResult[]} results The lint results to format.
629 * @param {ResultsMeta} resultsMeta Warning count and max threshold.
630 * @returns {string | Promise<string>} The formatted lint results.
632 format(results
, resultsMeta
) {
633 let rulesMeta
= null;
635 results
.sort(compareResultsByFilePath
);
637 return formatter(results
, {
644 rulesMeta
= createRulesMeta(cliEngine
.getRules());
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.
661 async
calculateConfigForFile(filePath
) {
662 if (!isNonEmptyString(filePath
)) {
663 throw new Error("'filePath' must be a non-empty string");
665 const { cliEngine
} = privateMembersMap
.get(this);
667 return cliEngine
.getConfigForFile(filePath
);
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.
675 async
isPathIgnored(filePath
) {
676 if (!isNonEmptyString(filePath
)) {
677 throw new Error("'filePath' must be a non-empty string");
679 const { cliEngine
} = privateMembersMap
.get(this);
681 return cliEngine
.isPathIgnored(filePath
);
685 //------------------------------------------------------------------------------
687 //------------------------------------------------------------------------------
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.
697 getESLintPrivateMembers(instance
) {
698 return privateMembersMap
.get(instance
);