flat-eslint.js 38 KB

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