eslint.js 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121
  1. /**
  2. * @fileoverview Main class using flat config
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const fs = require("node:fs/promises");
  10. const { existsSync } = require("node:fs");
  11. const path = require("node:path");
  12. const { version } = require("../../package.json");
  13. const { Linter } = require("../linter");
  14. const { getRuleFromConfig } = require("../config/flat-config-helpers");
  15. const { defaultConfig } = require("../config/default-config");
  16. const {
  17. Legacy: {
  18. ConfigOps: {
  19. getRuleSeverity
  20. },
  21. ModuleResolver,
  22. naming
  23. }
  24. } = require("@eslint/eslintrc");
  25. const {
  26. findFiles,
  27. getCacheFile,
  28. isNonEmptyString,
  29. isArrayOfNonEmptyString,
  30. createIgnoreResult,
  31. isErrorMessage,
  32. processOptions
  33. } = require("./eslint-helpers");
  34. const { pathToFileURL } = require("node:url");
  35. const LintResultCache = require("../cli-engine/lint-result-cache");
  36. const { Retrier } = require("@humanwhocodes/retry");
  37. const { ConfigLoader, LegacyConfigLoader } = require("../config/config-loader");
  38. /*
  39. * This is necessary to allow overwriting writeFile for testing purposes.
  40. * We can just use fs/promises once we drop Node.js 12 support.
  41. */
  42. //------------------------------------------------------------------------------
  43. // Typedefs
  44. //------------------------------------------------------------------------------
  45. // For VSCode IntelliSense
  46. /** @typedef {import("../cli-engine/cli-engine").ConfigArray} ConfigArray */
  47. /** @typedef {import("../shared/types").ConfigData} ConfigData */
  48. /** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */
  49. /** @typedef {import("../shared/types").LintMessage} LintMessage */
  50. /** @typedef {import("../shared/types").LintResult} LintResult */
  51. /** @typedef {import("../shared/types").ParserOptions} ParserOptions */
  52. /** @typedef {import("../shared/types").Plugin} Plugin */
  53. /** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */
  54. /** @typedef {import("../shared/types").RuleConf} RuleConf */
  55. /** @typedef {import("../shared/types").Rule} Rule */
  56. /** @typedef {ReturnType<ConfigArray.extractConfig>} ExtractedConfig */
  57. /** @typedef {import('../cli-engine/cli-engine').CLIEngine} CLIEngine */
  58. /** @typedef {import('./legacy-eslint').CLIEngineLintReport} CLIEngineLintReport */
  59. /**
  60. * The options with which to configure the ESLint instance.
  61. * @typedef {Object} ESLintOptions
  62. * @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments.
  63. * @property {ConfigData|Array<ConfigData>} [baseConfig] Base config, extended by all configs used with this instance
  64. * @property {boolean} [cache] Enable result caching.
  65. * @property {string} [cacheLocation] The cache file to use instead of .eslintcache.
  66. * @property {"metadata" | "content"} [cacheStrategy] The strategy used to detect changed files.
  67. * @property {string} [cwd] The value to use for the current working directory.
  68. * @property {boolean} [errorOnUnmatchedPattern] If `false` then `ESLint#lintFiles()` doesn't throw even if no target files found. Defaults to `true`.
  69. * @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean.
  70. * @property {string[]} [fixTypes] Array of rule types to apply fixes for.
  71. * @property {string[]} [flags] Array of feature flags to enable.
  72. * @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.
  73. * @property {boolean} [ignore] False disables all ignore patterns except for the default ones.
  74. * @property {string[]} [ignorePatterns] Ignore file patterns to use in addition to config ignores. These patterns are relative to `cwd`.
  75. * @property {ConfigData|Array<ConfigData>} [overrideConfig] Override config, overrides all configs used with this instance
  76. * @property {boolean|string} [overrideConfigFile] Searches for default config file when falsy;
  77. * doesn't do any config file lookup when `true`; considered to be a config filename
  78. * when a string.
  79. * @property {Record<string,Plugin>} [plugins] An array of plugin implementations.
  80. * @property {boolean} [stats] True enables added statistics on lint results.
  81. * @property {boolean} [warnIgnored] Show warnings when the file list includes ignored files
  82. * @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause
  83. * the linting operation to short circuit and not report any failures.
  84. */
  85. //------------------------------------------------------------------------------
  86. // Helpers
  87. //------------------------------------------------------------------------------
  88. const debug = require("debug")("eslint:eslint");
  89. const privateMembers = new WeakMap();
  90. const removedFormatters = new Set([
  91. "checkstyle",
  92. "codeframe",
  93. "compact",
  94. "jslint-xml",
  95. "junit",
  96. "table",
  97. "tap",
  98. "unix",
  99. "visualstudio"
  100. ]);
  101. /**
  102. * It will calculate the error and warning count for collection of messages per file
  103. * @param {LintMessage[]} messages Collection of messages
  104. * @returns {Object} Contains the stats
  105. * @private
  106. */
  107. function calculateStatsPerFile(messages) {
  108. const stat = {
  109. errorCount: 0,
  110. fatalErrorCount: 0,
  111. warningCount: 0,
  112. fixableErrorCount: 0,
  113. fixableWarningCount: 0
  114. };
  115. for (let i = 0; i < messages.length; i++) {
  116. const message = messages[i];
  117. if (message.fatal || message.severity === 2) {
  118. stat.errorCount++;
  119. if (message.fatal) {
  120. stat.fatalErrorCount++;
  121. }
  122. if (message.fix) {
  123. stat.fixableErrorCount++;
  124. }
  125. } else {
  126. stat.warningCount++;
  127. if (message.fix) {
  128. stat.fixableWarningCount++;
  129. }
  130. }
  131. }
  132. return stat;
  133. }
  134. /**
  135. * Create rulesMeta object.
  136. * @param {Map<string,Rule>} rules a map of rules from which to generate the object.
  137. * @returns {Object} metadata for all enabled rules.
  138. */
  139. function createRulesMeta(rules) {
  140. return Array.from(rules).reduce((retVal, [id, rule]) => {
  141. retVal[id] = rule.meta;
  142. return retVal;
  143. }, {});
  144. }
  145. /**
  146. * Return the absolute path of a file named `"__placeholder__.js"` in a given directory.
  147. * This is used as a replacement for a missing file path.
  148. * @param {string} cwd An absolute directory path.
  149. * @returns {string} The absolute path of a file named `"__placeholder__.js"` in the given directory.
  150. */
  151. function getPlaceholderPath(cwd) {
  152. return path.join(cwd, "__placeholder__.js");
  153. }
  154. /** @type {WeakMap<ExtractedConfig, DeprecatedRuleInfo[]>} */
  155. const usedDeprecatedRulesCache = new WeakMap();
  156. /**
  157. * Create used deprecated rule list.
  158. * @param {CLIEngine} eslint The CLIEngine instance.
  159. * @param {string} maybeFilePath The absolute path to a lint target file or `"<text>"`.
  160. * @returns {DeprecatedRuleInfo[]} The used deprecated rule list.
  161. */
  162. function getOrFindUsedDeprecatedRules(eslint, maybeFilePath) {
  163. const {
  164. options: { cwd },
  165. configLoader
  166. } = privateMembers.get(eslint);
  167. const filePath = path.isAbsolute(maybeFilePath)
  168. ? maybeFilePath
  169. : getPlaceholderPath(cwd);
  170. const configs = configLoader.getCachedConfigArrayForFile(filePath);
  171. const config = configs.getConfig(filePath);
  172. // Most files use the same config, so cache it.
  173. if (config && !usedDeprecatedRulesCache.has(config)) {
  174. const retv = [];
  175. if (config.rules) {
  176. for (const [ruleId, ruleConf] of Object.entries(config.rules)) {
  177. if (getRuleSeverity(ruleConf) === 0) {
  178. continue;
  179. }
  180. const rule = getRuleFromConfig(ruleId, config);
  181. const meta = rule && rule.meta;
  182. if (meta && meta.deprecated) {
  183. retv.push({ ruleId, replacedBy: meta.replacedBy || [] });
  184. }
  185. }
  186. }
  187. usedDeprecatedRulesCache.set(config, Object.freeze(retv));
  188. }
  189. return config ? usedDeprecatedRulesCache.get(config) : Object.freeze([]);
  190. }
  191. /**
  192. * Processes the linting results generated by a CLIEngine linting report to
  193. * match the ESLint class's API.
  194. * @param {CLIEngine} eslint The CLIEngine instance.
  195. * @param {CLIEngineLintReport} report The CLIEngine linting report to process.
  196. * @returns {LintResult[]} The processed linting results.
  197. */
  198. function processLintReport(eslint, { results }) {
  199. const descriptor = {
  200. configurable: true,
  201. enumerable: true,
  202. get() {
  203. return getOrFindUsedDeprecatedRules(eslint, this.filePath);
  204. }
  205. };
  206. for (const result of results) {
  207. Object.defineProperty(result, "usedDeprecatedRules", descriptor);
  208. }
  209. return results;
  210. }
  211. /**
  212. * An Array.prototype.sort() compatible compare function to order results by their file path.
  213. * @param {LintResult} a The first lint result.
  214. * @param {LintResult} b The second lint result.
  215. * @returns {number} An integer representing the order in which the two results should occur.
  216. */
  217. function compareResultsByFilePath(a, b) {
  218. if (a.filePath < b.filePath) {
  219. return -1;
  220. }
  221. if (a.filePath > b.filePath) {
  222. return 1;
  223. }
  224. return 0;
  225. }
  226. /**
  227. * Determines which config file to use. This is determined by seeing if an
  228. * override config file was passed, and if so, using it; otherwise, as long
  229. * as override config file is not explicitly set to `false`, it will search
  230. * upwards from the cwd for a file named `eslint.config.js`.
  231. *
  232. * This function is used primarily by the `--inspect-config` option. For now,
  233. * we will maintain the existing behavior, which is to search up from the cwd.
  234. * @param {ESLintOptions} options The ESLint instance options.
  235. * @param {boolean} allowTS `true` if the `unstable_ts_config` flag is enabled, `false` if it's not.
  236. * @returns {Promise<{configFilePath:string|undefined;basePath:string}>} Location information for
  237. * the config file.
  238. */
  239. async function locateConfigFileToUse({ configFile, cwd }, allowTS) {
  240. const configLoader = new ConfigLoader({
  241. cwd,
  242. allowTS,
  243. configFile
  244. });
  245. const configFilePath = await configLoader.findConfigFileForPath(path.join(cwd, "__placeholder__.js"));
  246. if (!configFilePath) {
  247. throw new Error("No ESLint configuration file was found.");
  248. }
  249. return {
  250. configFilePath,
  251. basePath: configFile ? cwd : path.dirname(configFilePath)
  252. };
  253. }
  254. /**
  255. * Processes an source code using ESLint.
  256. * @param {Object} config The config object.
  257. * @param {string} config.text The source code to verify.
  258. * @param {string} config.cwd The path to the current working directory.
  259. * @param {string|undefined} config.filePath The path to the file of `text`. If this is undefined, it uses `<text>`.
  260. * @param {FlatConfigArray} config.configs The config.
  261. * @param {boolean} config.fix If `true` then it does fix.
  262. * @param {boolean} config.allowInlineConfig If `true` then it uses directive comments.
  263. * @param {Function} config.ruleFilter A predicate function to filter which rules should be run.
  264. * @param {boolean} config.stats If `true`, then if reports extra statistics with the lint results.
  265. * @param {Linter} config.linter The linter instance to verify.
  266. * @returns {LintResult} The result of linting.
  267. * @private
  268. */
  269. function verifyText({
  270. text,
  271. cwd,
  272. filePath: providedFilePath,
  273. configs,
  274. fix,
  275. allowInlineConfig,
  276. ruleFilter,
  277. stats,
  278. linter
  279. }) {
  280. const filePath = providedFilePath || "<text>";
  281. debug(`Lint ${filePath}`);
  282. /*
  283. * Verify.
  284. * `config.extractConfig(filePath)` requires an absolute path, but `linter`
  285. * doesn't know CWD, so it gives `linter` an absolute path always.
  286. */
  287. const filePathToVerify = filePath === "<text>" ? getPlaceholderPath(cwd) : filePath;
  288. const { fixed, messages, output } = linter.verifyAndFix(
  289. text,
  290. configs,
  291. {
  292. allowInlineConfig,
  293. filename: filePathToVerify,
  294. fix,
  295. ruleFilter,
  296. stats,
  297. /**
  298. * Check if the linter should adopt a given code block or not.
  299. * @param {string} blockFilename The virtual filename of a code block.
  300. * @returns {boolean} `true` if the linter should adopt the code block.
  301. */
  302. filterCodeBlock(blockFilename) {
  303. return configs.getConfig(blockFilename) !== void 0;
  304. }
  305. }
  306. );
  307. // Tweak and return.
  308. const result = {
  309. filePath: filePath === "<text>" ? filePath : path.resolve(filePath),
  310. messages,
  311. suppressedMessages: linter.getSuppressedMessages(),
  312. ...calculateStatsPerFile(messages)
  313. };
  314. if (fixed) {
  315. result.output = output;
  316. }
  317. if (
  318. result.errorCount + result.warningCount > 0 &&
  319. typeof result.output === "undefined"
  320. ) {
  321. result.source = text;
  322. }
  323. if (stats) {
  324. result.stats = {
  325. times: linter.getTimes(),
  326. fixPasses: linter.getFixPassCount()
  327. };
  328. }
  329. return result;
  330. }
  331. /**
  332. * Checks whether a message's rule type should be fixed.
  333. * @param {LintMessage} message The message to check.
  334. * @param {FlatConfig} config The config for the file that generated the message.
  335. * @param {string[]} fixTypes An array of fix types to check.
  336. * @returns {boolean} Whether the message should be fixed.
  337. */
  338. function shouldMessageBeFixed(message, config, fixTypes) {
  339. if (!message.ruleId) {
  340. return fixTypes.has("directive");
  341. }
  342. const rule = message.ruleId && getRuleFromConfig(message.ruleId, config);
  343. return Boolean(rule && rule.meta && fixTypes.has(rule.meta.type));
  344. }
  345. /**
  346. * Creates an error to be thrown when an array of results passed to `getRulesMetaForResults` was not created by the current engine.
  347. * @returns {TypeError} An error object.
  348. */
  349. function createExtraneousResultsError() {
  350. return new TypeError("Results object was not created from this ESLint instance.");
  351. }
  352. /**
  353. * Creates a fixer function based on the provided fix, fixTypesSet, and config.
  354. * @param {Function|boolean} fix The original fix option.
  355. * @param {Set<string>} fixTypesSet A set of fix types to filter messages for fixing.
  356. * @param {FlatConfig} config The config for the file that generated the message.
  357. * @returns {Function|boolean} The fixer function or the original fix value.
  358. */
  359. function getFixerForFixTypes(fix, fixTypesSet, config) {
  360. if (!fix || !fixTypesSet) {
  361. return fix;
  362. }
  363. const originalFix = (typeof fix === "function") ? fix : () => true;
  364. return message => shouldMessageBeFixed(message, config, fixTypesSet) && originalFix(message);
  365. }
  366. //-----------------------------------------------------------------------------
  367. // Main API
  368. //-----------------------------------------------------------------------------
  369. /**
  370. * Primary Node.js API for ESLint.
  371. */
  372. class ESLint {
  373. /**
  374. * The type of configuration used by this class.
  375. * @type {string}
  376. */
  377. static configType = "flat";
  378. /**
  379. * The loader to use for finding config files.
  380. * @type {ConfigLoader|LegacyConfigLoader}
  381. */
  382. #configLoader;
  383. /**
  384. * Creates a new instance of the main ESLint API.
  385. * @param {ESLintOptions} options The options for this instance.
  386. */
  387. constructor(options = {}) {
  388. const defaultConfigs = [];
  389. const processedOptions = processOptions(options);
  390. const linter = new Linter({
  391. cwd: processedOptions.cwd,
  392. configType: "flat",
  393. flags: processedOptions.flags
  394. });
  395. const cacheFilePath = getCacheFile(
  396. processedOptions.cacheLocation,
  397. processedOptions.cwd
  398. );
  399. const lintResultCache = processedOptions.cache
  400. ? new LintResultCache(cacheFilePath, processedOptions.cacheStrategy)
  401. : null;
  402. const configLoaderOptions = {
  403. cwd: processedOptions.cwd,
  404. baseConfig: processedOptions.baseConfig,
  405. overrideConfig: processedOptions.overrideConfig,
  406. configFile: processedOptions.configFile,
  407. ignoreEnabled: processedOptions.ignore,
  408. ignorePatterns: processedOptions.ignorePatterns,
  409. defaultConfigs,
  410. allowTS: processedOptions.flags.includes("unstable_ts_config")
  411. };
  412. this.#configLoader = processedOptions.flags.includes("unstable_config_lookup_from_file")
  413. ? new ConfigLoader(configLoaderOptions)
  414. : new LegacyConfigLoader(configLoaderOptions);
  415. debug(`Using config loader ${this.#configLoader.constructor.name}`);
  416. privateMembers.set(this, {
  417. options: processedOptions,
  418. linter,
  419. cacheFilePath,
  420. lintResultCache,
  421. defaultConfigs,
  422. configs: null,
  423. configLoader: this.#configLoader
  424. });
  425. /**
  426. * If additional plugins are passed in, add that to the default
  427. * configs for this instance.
  428. */
  429. if (options.plugins) {
  430. const plugins = {};
  431. for (const [pluginName, plugin] of Object.entries(options.plugins)) {
  432. plugins[naming.getShorthandName(pluginName, "eslint-plugin")] = plugin;
  433. }
  434. defaultConfigs.push({
  435. plugins
  436. });
  437. }
  438. // Check for the .eslintignore file, and warn if it's present.
  439. if (existsSync(path.resolve(processedOptions.cwd, ".eslintignore"))) {
  440. process.emitWarning(
  441. "The \".eslintignore\" file is no longer supported. Switch to using the \"ignores\" property in \"eslint.config.js\": https://eslint.org/docs/latest/use/configure/migration-guide#ignoring-files",
  442. "ESLintIgnoreWarning"
  443. );
  444. }
  445. }
  446. /**
  447. * The version text.
  448. * @type {string}
  449. */
  450. static get version() {
  451. return version;
  452. }
  453. /**
  454. * The default configuration that ESLint uses internally. This is provided for tooling that wants to calculate configurations using the same defaults as ESLint.
  455. * Keep in mind that the default configuration may change from version to version, so you shouldn't rely on any particular keys or values to be present.
  456. * @type {ConfigArray}
  457. */
  458. static get defaultConfig() {
  459. return defaultConfig;
  460. }
  461. /**
  462. * Outputs fixes from the given results to files.
  463. * @param {LintResult[]} results The lint results.
  464. * @returns {Promise<void>} Returns a promise that is used to track side effects.
  465. */
  466. static async outputFixes(results) {
  467. if (!Array.isArray(results)) {
  468. throw new Error("'results' must be an array");
  469. }
  470. await Promise.all(
  471. results
  472. .filter(result => {
  473. if (typeof result !== "object" || result === null) {
  474. throw new Error("'results' must include only objects");
  475. }
  476. return (
  477. typeof result.output === "string" &&
  478. path.isAbsolute(result.filePath)
  479. );
  480. })
  481. .map(r => fs.writeFile(r.filePath, r.output))
  482. );
  483. }
  484. /**
  485. * Returns results that only contains errors.
  486. * @param {LintResult[]} results The results to filter.
  487. * @returns {LintResult[]} The filtered results.
  488. */
  489. static getErrorResults(results) {
  490. const filtered = [];
  491. results.forEach(result => {
  492. const filteredMessages = result.messages.filter(isErrorMessage);
  493. const filteredSuppressedMessages = result.suppressedMessages.filter(isErrorMessage);
  494. if (filteredMessages.length > 0) {
  495. filtered.push({
  496. ...result,
  497. messages: filteredMessages,
  498. suppressedMessages: filteredSuppressedMessages,
  499. errorCount: filteredMessages.length,
  500. warningCount: 0,
  501. fixableErrorCount: result.fixableErrorCount,
  502. fixableWarningCount: 0
  503. });
  504. }
  505. });
  506. return filtered;
  507. }
  508. /**
  509. * Returns meta objects for each rule represented in the lint results.
  510. * @param {LintResult[]} results The results to fetch rules meta for.
  511. * @returns {Object} A mapping of ruleIds to rule meta objects.
  512. * @throws {TypeError} When the results object wasn't created from this ESLint instance.
  513. * @throws {TypeError} When a plugin or rule is missing.
  514. */
  515. getRulesMetaForResults(results) {
  516. // short-circuit simple case
  517. if (results.length === 0) {
  518. return {};
  519. }
  520. const resultRules = new Map();
  521. const {
  522. configLoader,
  523. options: { cwd }
  524. } = privateMembers.get(this);
  525. for (const result of results) {
  526. /*
  527. * Normalize filename for <text>.
  528. */
  529. const filePath = result.filePath === "<text>"
  530. ? getPlaceholderPath(cwd) : result.filePath;
  531. const allMessages = result.messages.concat(result.suppressedMessages);
  532. for (const { ruleId } of allMessages) {
  533. if (!ruleId) {
  534. continue;
  535. }
  536. /*
  537. * All of the plugin and rule information is contained within the
  538. * calculated config for the given file.
  539. */
  540. let configs;
  541. try {
  542. configs = configLoader.getCachedConfigArrayForFile(filePath);
  543. } catch {
  544. throw createExtraneousResultsError();
  545. }
  546. const config = configs.getConfig(filePath);
  547. if (!config) {
  548. throw createExtraneousResultsError();
  549. }
  550. const rule = getRuleFromConfig(ruleId, config);
  551. // ignore unknown rules
  552. if (rule) {
  553. resultRules.set(ruleId, rule);
  554. }
  555. }
  556. }
  557. return createRulesMeta(resultRules);
  558. }
  559. /**
  560. * Indicates if the given feature flag is enabled for this instance.
  561. * @param {string} flag The feature flag to check.
  562. * @returns {boolean} `true` if the feature flag is enabled, `false` if not.
  563. */
  564. hasFlag(flag) {
  565. // note: Linter does validation of the flags
  566. return privateMembers.get(this).linter.hasFlag(flag);
  567. }
  568. /**
  569. * Executes the current configuration on an array of file and directory names.
  570. * @param {string|string[]} patterns An array of file and directory names.
  571. * @returns {Promise<LintResult[]>} The results of linting the file patterns given.
  572. */
  573. async lintFiles(patterns) {
  574. let normalizedPatterns = patterns;
  575. const {
  576. cacheFilePath,
  577. lintResultCache,
  578. linter,
  579. options: eslintOptions
  580. } = privateMembers.get(this);
  581. /*
  582. * Special cases:
  583. * 1. `patterns` is an empty string
  584. * 2. `patterns` is an empty array
  585. *
  586. * In both cases, we use the cwd as the directory to lint.
  587. */
  588. if (patterns === "" || Array.isArray(patterns) && patterns.length === 0) {
  589. /*
  590. * Special case: If `passOnNoPatterns` is true, then we just exit
  591. * without doing any work.
  592. */
  593. if (eslintOptions.passOnNoPatterns) {
  594. return [];
  595. }
  596. normalizedPatterns = ["."];
  597. } else {
  598. if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) {
  599. throw new Error("'patterns' must be a non-empty string or an array of non-empty strings");
  600. }
  601. if (typeof patterns === "string") {
  602. normalizedPatterns = [patterns];
  603. }
  604. }
  605. debug(`Using file patterns: ${normalizedPatterns}`);
  606. const {
  607. allowInlineConfig,
  608. cache,
  609. cwd,
  610. fix,
  611. fixTypes,
  612. ruleFilter,
  613. stats,
  614. globInputPaths,
  615. errorOnUnmatchedPattern,
  616. warnIgnored
  617. } = eslintOptions;
  618. const startTime = Date.now();
  619. const fixTypesSet = fixTypes ? new Set(fixTypes) : null;
  620. // Delete cache file; should this be done here?
  621. if (!cache && cacheFilePath) {
  622. debug(`Deleting cache file at ${cacheFilePath}`);
  623. try {
  624. await fs.unlink(cacheFilePath);
  625. } catch (error) {
  626. const errorCode = error && error.code;
  627. // Ignore errors when no such file exists or file system is read only (and cache file does not exist)
  628. if (errorCode !== "ENOENT" && !(errorCode === "EROFS" && !existsSync(cacheFilePath))) {
  629. throw error;
  630. }
  631. }
  632. }
  633. const filePaths = await findFiles({
  634. patterns: normalizedPatterns,
  635. cwd,
  636. globInputPaths,
  637. configLoader: this.#configLoader,
  638. errorOnUnmatchedPattern
  639. });
  640. const controller = new AbortController();
  641. const retryCodes = new Set(["ENFILE", "EMFILE"]);
  642. const retrier = new Retrier(error => retryCodes.has(error.code), { concurrency: 100 });
  643. debug(`${filePaths.length} files found in: ${Date.now() - startTime}ms`);
  644. /*
  645. * Because we need to process multiple files, including reading from disk,
  646. * it is most efficient to start by reading each file via promises so that
  647. * they can be done in parallel. Then, we can lint the returned text. This
  648. * ensures we are waiting the minimum amount of time in between lints.
  649. */
  650. const results = await Promise.all(
  651. filePaths.map(async filePath => {
  652. const configs = await this.#configLoader.loadConfigArrayForFile(filePath);
  653. const config = configs.getConfig(filePath);
  654. /*
  655. * If a filename was entered that cannot be matched
  656. * to a config, then notify the user.
  657. */
  658. if (!config) {
  659. if (warnIgnored) {
  660. const configStatus = configs.getConfigStatus(filePath);
  661. return createIgnoreResult(filePath, cwd, configStatus);
  662. }
  663. return void 0;
  664. }
  665. // Skip if there is cached result.
  666. if (lintResultCache) {
  667. const cachedResult =
  668. lintResultCache.getCachedLintResults(filePath, config);
  669. if (cachedResult) {
  670. const hadMessages =
  671. cachedResult.messages &&
  672. cachedResult.messages.length > 0;
  673. if (hadMessages && fix) {
  674. debug(`Reprocessing cached file to allow autofix: ${filePath}`);
  675. } else {
  676. debug(`Skipping file since it hasn't changed: ${filePath}`);
  677. return cachedResult;
  678. }
  679. }
  680. }
  681. // set up fixer for fixTypes if necessary
  682. const fixer = getFixerForFixTypes(fix, fixTypesSet, config);
  683. return retrier.retry(() => fs.readFile(filePath, { encoding: "utf8", signal: controller.signal })
  684. .then(text => {
  685. // fail immediately if an error occurred in another file
  686. controller.signal.throwIfAborted();
  687. // do the linting
  688. const result = verifyText({
  689. text,
  690. filePath,
  691. configs,
  692. cwd,
  693. fix: fixer,
  694. allowInlineConfig,
  695. ruleFilter,
  696. stats,
  697. linter
  698. });
  699. /*
  700. * Store the lint result in the LintResultCache.
  701. * NOTE: The LintResultCache will remove the file source and any
  702. * other properties that are difficult to serialize, and will
  703. * hydrate those properties back in on future lint runs.
  704. */
  705. if (lintResultCache) {
  706. lintResultCache.setCachedLintResults(filePath, config, result);
  707. }
  708. return result;
  709. }), { signal: controller.signal })
  710. .catch(error => {
  711. controller.abort(error);
  712. throw error;
  713. });
  714. })
  715. );
  716. // Persist the cache to disk.
  717. if (lintResultCache) {
  718. lintResultCache.reconcile();
  719. }
  720. const finalResults = results.filter(result => !!result);
  721. return processLintReport(this, {
  722. results: finalResults
  723. });
  724. }
  725. /**
  726. * Executes the current configuration on text.
  727. * @param {string} code A string of JavaScript code to lint.
  728. * @param {Object} [options] The options.
  729. * @param {string} [options.filePath] The path to the file of the source code.
  730. * @param {boolean} [options.warnIgnored] When set to true, warn if given filePath is an ignored path.
  731. * @returns {Promise<LintResult[]>} The results of linting the string of code given.
  732. */
  733. async lintText(code, options = {}) {
  734. // Parameter validation
  735. if (typeof code !== "string") {
  736. throw new Error("'code' must be a string");
  737. }
  738. if (typeof options !== "object") {
  739. throw new Error("'options' must be an object, null, or undefined");
  740. }
  741. // Options validation
  742. const {
  743. filePath,
  744. warnIgnored,
  745. ...unknownOptions
  746. } = options || {};
  747. const unknownOptionKeys = Object.keys(unknownOptions);
  748. if (unknownOptionKeys.length > 0) {
  749. throw new Error(`'options' must not include the unknown option(s): ${unknownOptionKeys.join(", ")}`);
  750. }
  751. if (filePath !== void 0 && !isNonEmptyString(filePath)) {
  752. throw new Error("'options.filePath' must be a non-empty string or undefined");
  753. }
  754. if (typeof warnIgnored !== "boolean" && typeof warnIgnored !== "undefined") {
  755. throw new Error("'options.warnIgnored' must be a boolean or undefined");
  756. }
  757. // Now we can get down to linting
  758. const {
  759. linter,
  760. options: eslintOptions
  761. } = privateMembers.get(this);
  762. const {
  763. allowInlineConfig,
  764. cwd,
  765. fix,
  766. fixTypes,
  767. warnIgnored: constructorWarnIgnored,
  768. ruleFilter,
  769. stats
  770. } = eslintOptions;
  771. const results = [];
  772. const startTime = Date.now();
  773. const fixTypesSet = fixTypes ? new Set(fixTypes) : null;
  774. const resolvedFilename = path.resolve(cwd, filePath || "__placeholder__.js");
  775. const configs = await this.#configLoader.loadConfigArrayForFile(resolvedFilename);
  776. const configStatus = configs?.getConfigStatus(resolvedFilename) ?? "unconfigured";
  777. // Clear the last used config arrays.
  778. if (resolvedFilename && configStatus !== "matched") {
  779. const shouldWarnIgnored = typeof warnIgnored === "boolean" ? warnIgnored : constructorWarnIgnored;
  780. if (shouldWarnIgnored) {
  781. results.push(createIgnoreResult(resolvedFilename, cwd, configStatus));
  782. }
  783. } else {
  784. const config = configs.getConfig(resolvedFilename);
  785. const fixer = getFixerForFixTypes(fix, fixTypesSet, config);
  786. // Do lint.
  787. results.push(verifyText({
  788. text: code,
  789. filePath: resolvedFilename.endsWith("__placeholder__.js") ? "<text>" : resolvedFilename,
  790. configs,
  791. cwd,
  792. fix: fixer,
  793. allowInlineConfig,
  794. ruleFilter,
  795. stats,
  796. linter
  797. }));
  798. }
  799. debug(`Linting complete in: ${Date.now() - startTime}ms`);
  800. return processLintReport(this, {
  801. results
  802. });
  803. }
  804. /**
  805. * Returns the formatter representing the given formatter name.
  806. * @param {string} [name] The name of the formatter to load.
  807. * The following values are allowed:
  808. * - `undefined` ... Load `stylish` builtin formatter.
  809. * - A builtin formatter name ... Load the builtin formatter.
  810. * - A third-party formatter name:
  811. * - `foo` → `eslint-formatter-foo`
  812. * - `@foo` → `@foo/eslint-formatter`
  813. * - `@foo/bar` → `@foo/eslint-formatter-bar`
  814. * - A file path ... Load the file.
  815. * @returns {Promise<Formatter>} A promise resolving to the formatter object.
  816. * This promise will be rejected if the given formatter was not found or not
  817. * a function.
  818. */
  819. async loadFormatter(name = "stylish") {
  820. if (typeof name !== "string") {
  821. throw new Error("'name' must be a string");
  822. }
  823. // replace \ with / for Windows compatibility
  824. const normalizedFormatName = name.replace(/\\/gu, "/");
  825. const namespace = naming.getNamespaceFromTerm(normalizedFormatName);
  826. // grab our options
  827. const { cwd } = privateMembers.get(this).options;
  828. let formatterPath;
  829. // if there's a slash, then it's a file (TODO: this check seems dubious for scoped npm packages)
  830. if (!namespace && normalizedFormatName.includes("/")) {
  831. formatterPath = path.resolve(cwd, normalizedFormatName);
  832. } else {
  833. try {
  834. const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter");
  835. // TODO: This is pretty dirty...would be nice to clean up at some point.
  836. formatterPath = ModuleResolver.resolve(npmFormat, getPlaceholderPath(cwd));
  837. } catch {
  838. formatterPath = path.resolve(__dirname, "../", "cli-engine", "formatters", `${normalizedFormatName}.js`);
  839. }
  840. }
  841. let formatter;
  842. try {
  843. formatter = (await import(pathToFileURL(formatterPath))).default;
  844. } catch (ex) {
  845. // check for formatters that have been removed
  846. if (removedFormatters.has(name)) {
  847. ex.message = `The ${name} formatter is no longer part of core ESLint. Install it manually with \`npm install -D eslint-formatter-${name}\``;
  848. } else {
  849. ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
  850. }
  851. throw ex;
  852. }
  853. if (typeof formatter !== "function") {
  854. throw new TypeError(`Formatter must be a function, but got a ${typeof formatter}.`);
  855. }
  856. const eslint = this;
  857. return {
  858. /**
  859. * The main formatter method.
  860. * @param {LintResult[]} results The lint results to format.
  861. * @param {ResultsMeta} resultsMeta Warning count and max threshold.
  862. * @returns {string} The formatted lint results.
  863. */
  864. format(results, resultsMeta) {
  865. let rulesMeta = null;
  866. results.sort(compareResultsByFilePath);
  867. return formatter(results, {
  868. ...resultsMeta,
  869. cwd,
  870. get rulesMeta() {
  871. if (!rulesMeta) {
  872. rulesMeta = eslint.getRulesMetaForResults(results);
  873. }
  874. return rulesMeta;
  875. }
  876. });
  877. }
  878. };
  879. }
  880. /**
  881. * Returns a configuration object for the given file based on the CLI options.
  882. * This is the same logic used by the ESLint CLI executable to determine
  883. * configuration for each file it processes.
  884. * @param {string} filePath The path of the file to retrieve a config object for.
  885. * @returns {Promise<ConfigData|undefined>} A configuration object for the file
  886. * or `undefined` if there is no configuration data for the object.
  887. */
  888. async calculateConfigForFile(filePath) {
  889. if (!isNonEmptyString(filePath)) {
  890. throw new Error("'filePath' must be a non-empty string");
  891. }
  892. const options = privateMembers.get(this).options;
  893. const absolutePath = path.resolve(options.cwd, filePath);
  894. const configs = await this.#configLoader.loadConfigArrayForFile(absolutePath);
  895. if (!configs) {
  896. const error = new Error("Could not find config file.");
  897. error.messageTemplate = "config-file-missing";
  898. throw error;
  899. }
  900. return configs.getConfig(absolutePath);
  901. }
  902. /**
  903. * Finds the config file being used by this instance based on the options
  904. * passed to the constructor.
  905. * @param {string} [filePath] The path of the file to find the config file for.
  906. * @returns {Promise<string|undefined>} The path to the config file being used or
  907. * `undefined` if no config file is being used.
  908. */
  909. findConfigFile(filePath) {
  910. const options = privateMembers.get(this).options;
  911. /*
  912. * Because the new config lookup scheme skips the current directory
  913. * and looks into the parent directories, we need to use a placeholder
  914. * directory to ensure the file in cwd is checked.
  915. */
  916. const fakeCwd = path.join(options.cwd, "__placeholder__");
  917. return this.#configLoader.findConfigFileForPath(filePath ?? fakeCwd)
  918. .catch(() => void 0);
  919. }
  920. /**
  921. * Checks if a given path is ignored by ESLint.
  922. * @param {string} filePath The path of the file to check.
  923. * @returns {Promise<boolean>} Whether or not the given path is ignored.
  924. */
  925. async isPathIgnored(filePath) {
  926. const config = await this.calculateConfigForFile(filePath);
  927. return config === void 0;
  928. }
  929. }
  930. /**
  931. * Returns whether flat config should be used.
  932. * @returns {Promise<boolean>} Whether flat config should be used.
  933. */
  934. async function shouldUseFlatConfig() {
  935. return (process.env.ESLINT_USE_FLAT_CONFIG !== "false");
  936. }
  937. //------------------------------------------------------------------------------
  938. // Public Interface
  939. //------------------------------------------------------------------------------
  940. module.exports = {
  941. ESLint,
  942. shouldUseFlatConfig,
  943. locateConfigFileToUse
  944. };