]> git.proxmox.com Git - pve-eslint.git/blame - eslint/lib/eslint/eslint.js
import 7.12.1 upstream release
[pve-eslint.git] / eslint / lib / eslint / eslint.js
CommitLineData
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
13const path = require("path");
14const fs = require("fs");
15const { promisify } = require("util");
16const { CLIEngine, getCLIEngineInternalSlots } = require("../cli-engine/cli-engine");
17const BuiltinRules = require("../rules");
6f036462
TL
18const {
19 Legacy: {
20 ConfigOps: {
21 getRuleSeverity
22 }
23 }
24} = require("@eslint/eslintrc");
56c4a2cb
DC
25const { 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
95const writeFile = promisify(fs.writeFile);
96
97/**
98 * The map with which to store private class members.
99 * @type {WeakMap<ESLint, ESLintPrivateMembers>}
100 */
101const 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 */
108function 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 */
117function 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 */
126function 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 */
135function isFixTypeArray(x) {
136 return Array.isArray(x) && x.every(isFixType);
137}
138
139/**
140 * The error for invalid options.
141 */
142class 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 */
155function 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 */
308function 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 */
324function 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[]>} */
332const 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 */
340function 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 */
381function 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 */
403function 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
415class 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
651module.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};