eslint-helpers.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  1. /**
  2. * @fileoverview Helper functions for ESLint class
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //-----------------------------------------------------------------------------
  7. // Requirements
  8. //-----------------------------------------------------------------------------
  9. const path = require("node:path");
  10. const fs = require("node:fs");
  11. const fsp = fs.promises;
  12. const isGlob = require("is-glob");
  13. const hash = require("../cli-engine/hash");
  14. const minimatch = require("minimatch");
  15. const globParent = require("glob-parent");
  16. //-----------------------------------------------------------------------------
  17. // Fixup references
  18. //-----------------------------------------------------------------------------
  19. const Minimatch = minimatch.Minimatch;
  20. const MINIMATCH_OPTIONS = { dot: true };
  21. //-----------------------------------------------------------------------------
  22. // Types
  23. //-----------------------------------------------------------------------------
  24. /**
  25. * @typedef {Object} GlobSearch
  26. * @property {Array<string>} patterns The normalized patterns to use for a search.
  27. * @property {Array<string>} rawPatterns The patterns as entered by the user
  28. * before doing any normalization.
  29. */
  30. //-----------------------------------------------------------------------------
  31. // Errors
  32. //-----------------------------------------------------------------------------
  33. /**
  34. * The error type when no files match a glob.
  35. */
  36. class NoFilesFoundError extends Error {
  37. /**
  38. * @param {string} pattern The glob pattern which was not found.
  39. * @param {boolean} globEnabled If `false` then the pattern was a glob pattern, but glob was disabled.
  40. */
  41. constructor(pattern, globEnabled) {
  42. super(`No files matching '${pattern}' were found${!globEnabled ? " (glob was disabled)" : ""}.`);
  43. this.messageTemplate = "file-not-found";
  44. this.messageData = { pattern, globDisabled: !globEnabled };
  45. }
  46. }
  47. /**
  48. * The error type when a search fails to match multiple patterns.
  49. */
  50. class UnmatchedSearchPatternsError extends Error {
  51. /**
  52. * @param {Object} options The options for the error.
  53. * @param {string} options.basePath The directory that was searched.
  54. * @param {Array<string>} options.unmatchedPatterns The glob patterns
  55. * which were not found.
  56. * @param {Array<string>} options.patterns The glob patterns that were
  57. * searched.
  58. * @param {Array<string>} options.rawPatterns The raw glob patterns that
  59. * were searched.
  60. */
  61. constructor({ basePath, unmatchedPatterns, patterns, rawPatterns }) {
  62. super(`No files matching '${rawPatterns}' in '${basePath}' were found.`);
  63. this.basePath = basePath;
  64. this.unmatchedPatterns = unmatchedPatterns;
  65. this.patterns = patterns;
  66. this.rawPatterns = rawPatterns;
  67. }
  68. }
  69. /**
  70. * The error type when there are files matched by a glob, but all of them have been ignored.
  71. */
  72. class AllFilesIgnoredError extends Error {
  73. /**
  74. * @param {string} pattern The glob pattern which was not found.
  75. */
  76. constructor(pattern) {
  77. super(`All files matched by '${pattern}' are ignored.`);
  78. this.messageTemplate = "all-matched-files-ignored";
  79. this.messageData = { pattern };
  80. }
  81. }
  82. //-----------------------------------------------------------------------------
  83. // General Helpers
  84. //-----------------------------------------------------------------------------
  85. /**
  86. * Check if a given value is a non-empty string or not.
  87. * @param {any} value The value to check.
  88. * @returns {boolean} `true` if `value` is a non-empty string.
  89. */
  90. function isNonEmptyString(value) {
  91. return typeof value === "string" && value.trim() !== "";
  92. }
  93. /**
  94. * Check if a given value is an array of non-empty strings or not.
  95. * @param {any} value The value to check.
  96. * @returns {boolean} `true` if `value` is an array of non-empty strings.
  97. */
  98. function isArrayOfNonEmptyString(value) {
  99. return Array.isArray(value) && value.length && value.every(isNonEmptyString);
  100. }
  101. /**
  102. * Check if a given value is an empty array or an array of non-empty strings.
  103. * @param {any} value The value to check.
  104. * @returns {boolean} `true` if `value` is an empty array or an array of non-empty
  105. * strings.
  106. */
  107. function isEmptyArrayOrArrayOfNonEmptyString(value) {
  108. return Array.isArray(value) && value.every(isNonEmptyString);
  109. }
  110. //-----------------------------------------------------------------------------
  111. // File-related Helpers
  112. //-----------------------------------------------------------------------------
  113. /**
  114. * Normalizes slashes in a file pattern to posix-style.
  115. * @param {string} pattern The pattern to replace slashes in.
  116. * @returns {string} The pattern with slashes normalized.
  117. */
  118. function normalizeToPosix(pattern) {
  119. return pattern.replace(/\\/gu, "/");
  120. }
  121. /**
  122. * Check if a string is a glob pattern or not.
  123. * @param {string} pattern A glob pattern.
  124. * @returns {boolean} `true` if the string is a glob pattern.
  125. */
  126. function isGlobPattern(pattern) {
  127. return isGlob(path.sep === "\\" ? normalizeToPosix(pattern) : pattern);
  128. }
  129. /**
  130. * Determines if a given glob pattern will return any results.
  131. * Used primarily to help with useful error messages.
  132. * @param {Object} options The options for the function.
  133. * @param {string} options.basePath The directory to search.
  134. * @param {string} options.pattern An absolute path glob pattern to match.
  135. * @returns {Promise<boolean>} True if there is a glob match, false if not.
  136. */
  137. async function globMatch({ basePath, pattern }) {
  138. let found = false;
  139. const { hfs } = await import("@humanfs/node");
  140. const patternToUse = normalizeToPosix(path.relative(basePath, pattern));
  141. const matcher = new Minimatch(patternToUse, MINIMATCH_OPTIONS);
  142. const walkSettings = {
  143. directoryFilter(entry) {
  144. return !found && matcher.match(entry.path, true);
  145. },
  146. entryFilter(entry) {
  147. if (found || entry.isDirectory) {
  148. return false;
  149. }
  150. if (matcher.match(entry.path)) {
  151. found = true;
  152. return true;
  153. }
  154. return false;
  155. }
  156. };
  157. if (await hfs.isDirectory(basePath)) {
  158. return hfs.walk(basePath, walkSettings).next().then(() => found);
  159. }
  160. return found;
  161. }
  162. /**
  163. * Searches a directory looking for matching glob patterns. This uses
  164. * the config array's logic to determine if a directory or file should
  165. * be ignored, so it is consistent with how ignoring works throughout
  166. * ESLint.
  167. * @param {Object} options The options for this function.
  168. * @param {string} options.basePath The directory to search.
  169. * @param {Array<string>} options.patterns An array of absolute path glob patterns
  170. * to match.
  171. * @param {Array<string>} options.rawPatterns An array of glob patterns
  172. * as the user inputted them. Used for errors.
  173. * @param {ConfigLoader|LegacyConfigLoader} options.configLoader The config array to use for
  174. * determining what to ignore.
  175. * @param {boolean} options.errorOnUnmatchedPattern Determines if an error
  176. * should be thrown when a pattern is unmatched.
  177. * @returns {Promise<Array<string>>} An array of matching file paths
  178. * or an empty array if there are no matches.
  179. * @throws {UnmatchedSearchPatternsError} If there is a pattern that doesn't
  180. * match any files.
  181. */
  182. async function globSearch({
  183. basePath,
  184. patterns,
  185. rawPatterns,
  186. configLoader,
  187. errorOnUnmatchedPattern
  188. }) {
  189. if (patterns.length === 0) {
  190. return [];
  191. }
  192. /*
  193. * In this section we are converting the patterns into Minimatch
  194. * instances for performance reasons. Because we are doing the same
  195. * matches repeatedly, it's best to compile those patterns once and
  196. * reuse them multiple times.
  197. *
  198. * To do that, we convert any patterns with an absolute path into a
  199. * relative path and normalize it to Posix-style slashes. We also keep
  200. * track of the relative patterns to map them back to the original
  201. * patterns, which we need in order to throw an error if there are any
  202. * unmatched patterns.
  203. */
  204. const relativeToPatterns = new Map();
  205. const matchers = patterns.map((pattern, i) => {
  206. const patternToUse = normalizeToPosix(path.relative(basePath, pattern));
  207. relativeToPatterns.set(patternToUse, patterns[i]);
  208. return new Minimatch(patternToUse, MINIMATCH_OPTIONS);
  209. });
  210. /*
  211. * We track unmatched patterns because we may want to throw an error when
  212. * they occur. To start, this set is initialized with all of the patterns.
  213. * Every time a match occurs, the pattern is removed from the set, making
  214. * it easy to tell if we have any unmatched patterns left at the end of
  215. * search.
  216. */
  217. const unmatchedPatterns = new Set([...relativeToPatterns.keys()]);
  218. const { hfs } = await import("@humanfs/node");
  219. const walk = hfs.walk(
  220. basePath,
  221. {
  222. async directoryFilter(entry) {
  223. if (!matchers.some(matcher => matcher.match(entry.path, true))) {
  224. return false;
  225. }
  226. const absolutePath = path.resolve(basePath, entry.path);
  227. const configs = await configLoader.loadConfigArrayForDirectory(absolutePath);
  228. return !configs.isDirectoryIgnored(absolutePath);
  229. },
  230. async entryFilter(entry) {
  231. const absolutePath = path.resolve(basePath, entry.path);
  232. // entries may be directories or files so filter out directories
  233. if (entry.isDirectory) {
  234. return false;
  235. }
  236. const configs = await configLoader.loadConfigArrayForFile(absolutePath);
  237. const config = configs.getConfig(absolutePath);
  238. /*
  239. * Optimization: We need to track when patterns are left unmatched
  240. * and so we use `unmatchedPatterns` to do that. There is a bit of
  241. * complexity here because the same file can be matched by more than
  242. * one pattern. So, when we start, we actually need to test every
  243. * pattern against every file. Once we know there are no remaining
  244. * unmatched patterns, then we can switch to just looking for the
  245. * first matching pattern for improved speed.
  246. */
  247. const matchesPattern = unmatchedPatterns.size > 0
  248. ? matchers.reduce((previousValue, matcher) => {
  249. const pathMatches = matcher.match(entry.path);
  250. /*
  251. * We updated the unmatched patterns set only if the path
  252. * matches and the file has a config. If the file has no
  253. * config, that means there wasn't a match for the
  254. * pattern so it should not be removed.
  255. *
  256. * Performance note: `getConfig()` aggressively caches
  257. * results so there is no performance penalty for calling
  258. * it multiple times with the same argument.
  259. */
  260. if (pathMatches && config) {
  261. unmatchedPatterns.delete(matcher.pattern);
  262. }
  263. return pathMatches || previousValue;
  264. }, false)
  265. : matchers.some(matcher => matcher.match(entry.path));
  266. return matchesPattern && config !== void 0;
  267. }
  268. }
  269. );
  270. const filePaths = [];
  271. if (await hfs.isDirectory(basePath)) {
  272. for await (const entry of walk) {
  273. filePaths.push(path.resolve(basePath, entry.path));
  274. }
  275. }
  276. // now check to see if we have any unmatched patterns
  277. if (errorOnUnmatchedPattern && unmatchedPatterns.size > 0) {
  278. throw new UnmatchedSearchPatternsError({
  279. basePath,
  280. unmatchedPatterns: [...unmatchedPatterns].map(
  281. pattern => relativeToPatterns.get(pattern)
  282. ),
  283. patterns,
  284. rawPatterns
  285. });
  286. }
  287. return filePaths;
  288. }
  289. /**
  290. * Throws an error for unmatched patterns. The error will only contain information about the first one.
  291. * Checks to see if there are any ignored results for a given search.
  292. * @param {Object} options The options for this function.
  293. * @param {string} options.basePath The directory to search.
  294. * @param {Array<string>} options.patterns An array of glob patterns
  295. * that were used in the original search.
  296. * @param {Array<string>} options.rawPatterns An array of glob patterns
  297. * as the user inputted them. Used for errors.
  298. * @param {Array<string>} options.unmatchedPatterns A non-empty array of absolute path glob patterns
  299. * that were unmatched in the original search.
  300. * @returns {void} Always throws an error.
  301. * @throws {NoFilesFoundError} If the first unmatched pattern
  302. * doesn't match any files even when there are no ignores.
  303. * @throws {AllFilesIgnoredError} If the first unmatched pattern
  304. * matches some files when there are no ignores.
  305. */
  306. async function throwErrorForUnmatchedPatterns({
  307. basePath,
  308. patterns,
  309. rawPatterns,
  310. unmatchedPatterns
  311. }) {
  312. const pattern = unmatchedPatterns[0];
  313. const rawPattern = rawPatterns[patterns.indexOf(pattern)];
  314. const patternHasMatch = await globMatch({
  315. basePath,
  316. pattern
  317. });
  318. if (patternHasMatch) {
  319. throw new AllFilesIgnoredError(rawPattern);
  320. }
  321. // if we get here there are truly no matches
  322. throw new NoFilesFoundError(rawPattern, true);
  323. }
  324. /**
  325. * Performs multiple glob searches in parallel.
  326. * @param {Object} options The options for this function.
  327. * @param {Map<string,GlobSearch>} options.searches
  328. * A map of absolute path glob patterns to match.
  329. * @param {ConfigLoader|LegacyConfigLoader} options.configLoader The config loader to use for
  330. * determining what to ignore.
  331. * @param {boolean} options.errorOnUnmatchedPattern Determines if an
  332. * unmatched glob pattern should throw an error.
  333. * @returns {Promise<Array<string>>} An array of matching file paths
  334. * or an empty array if there are no matches.
  335. */
  336. async function globMultiSearch({ searches, configLoader, errorOnUnmatchedPattern }) {
  337. /*
  338. * For convenience, we normalized the search map into an array of objects.
  339. * Next, we filter out all searches that have no patterns. This happens
  340. * primarily for the cwd, which is prepopulated in the searches map as an
  341. * optimization. However, if it has no patterns, it means all patterns
  342. * occur outside of the cwd and we can safely filter out that search.
  343. */
  344. const normalizedSearches = [...searches].map(
  345. ([basePath, { patterns, rawPatterns }]) => ({ basePath, patterns, rawPatterns })
  346. ).filter(({ patterns }) => patterns.length > 0);
  347. const results = await Promise.allSettled(
  348. normalizedSearches.map(
  349. ({ basePath, patterns, rawPatterns }) => globSearch({
  350. basePath,
  351. patterns,
  352. rawPatterns,
  353. configLoader,
  354. errorOnUnmatchedPattern
  355. })
  356. )
  357. );
  358. /*
  359. * The first loop handles errors from the glob searches. Since we can't
  360. * use `await` inside `flatMap`, we process errors separately in this loop.
  361. * This results in two iterations over `results`, but since the length is
  362. * less than or equal to the number of globs and directories passed on the
  363. * command line, the performance impact should be minimal.
  364. */
  365. for (let i = 0; i < results.length; i++) {
  366. const result = results[i];
  367. const currentSearch = normalizedSearches[i];
  368. if (result.status === "fulfilled") {
  369. continue;
  370. }
  371. // if we make it here then there was an error
  372. const error = result.reason;
  373. // unexpected errors should be re-thrown
  374. if (!error.basePath) {
  375. throw error;
  376. }
  377. if (errorOnUnmatchedPattern) {
  378. await throwErrorForUnmatchedPatterns({
  379. ...currentSearch,
  380. unmatchedPatterns: error.unmatchedPatterns
  381. });
  382. }
  383. }
  384. // second loop for `fulfulled` results
  385. return results.flatMap(result => result.value);
  386. }
  387. /**
  388. * Finds all files matching the options specified.
  389. * @param {Object} args The arguments objects.
  390. * @param {Array<string>} args.patterns An array of glob patterns.
  391. * @param {boolean} args.globInputPaths true to interpret glob patterns,
  392. * false to not interpret glob patterns.
  393. * @param {string} args.cwd The current working directory to find from.
  394. * @param {ConfigLoader|LegacyConfigLoader} args.configLoader The config loeader for the current run.
  395. * @param {boolean} args.errorOnUnmatchedPattern Determines if an unmatched pattern
  396. * should throw an error.
  397. * @returns {Promise<Array<string>>} The fully resolved file paths.
  398. * @throws {AllFilesIgnoredError} If there are no results due to an ignore pattern.
  399. * @throws {NoFilesFoundError} If no files matched the given patterns.
  400. */
  401. async function findFiles({
  402. patterns,
  403. globInputPaths,
  404. cwd,
  405. configLoader,
  406. errorOnUnmatchedPattern
  407. }) {
  408. const results = [];
  409. const missingPatterns = [];
  410. let globbyPatterns = [];
  411. let rawPatterns = [];
  412. const searches = new Map([[cwd, { patterns: globbyPatterns, rawPatterns: [] }]]);
  413. /*
  414. * This part is a bit involved because we need to account for
  415. * the different ways that the patterns can match directories.
  416. * For each different way, we need to decide if we should look
  417. * for a config file or just use the default config. (Directories
  418. * without a config file always use the default config.)
  419. *
  420. * Here are the cases:
  421. *
  422. * 1. A directory is passed directly (e.g., "subdir"). In this case, we
  423. * can assume that the user intends to lint this directory and we should
  424. * not look for a config file in the parent directory, because the only
  425. * reason to do that would be to ignore this directory (which we already
  426. * know we don't want to do). Instead, we use the default config until we
  427. * get to the directory that was passed, at which point we start looking
  428. * for config files again.
  429. *
  430. * 2. A dot (".") or star ("*"). In this case, we want to read
  431. * the config file in the current directory because the user is
  432. * explicitly asking to lint the current directory. Note that "."
  433. * will traverse into subdirectories while "*" will not.
  434. *
  435. * 3. A directory is passed in the form of "subdir/subsubdir".
  436. * In this case, we don't want to look for a config file in the
  437. * parent directory ("subdir"). We can skip looking for a config
  438. * file until `entry.depth` is greater than 1 because there's no
  439. * way that the pattern can match `entry.path` yet.
  440. *
  441. * 4. A directory glob pattern is passed (e.g., "subd*"). We want
  442. * this case to act like case 2 because it's unclear whether or not
  443. * any particular directory is meant to be traversed.
  444. *
  445. * 5. A recursive glob pattern is passed (e.g., "**"). We want this
  446. * case to act like case 2.
  447. */
  448. // check to see if we have explicit files and directories
  449. const filePaths = patterns.map(filePath => path.resolve(cwd, filePath));
  450. const stats = await Promise.all(
  451. filePaths.map(
  452. filePath => fsp.stat(filePath).catch(() => { })
  453. )
  454. );
  455. stats.forEach((stat, index) => {
  456. const filePath = filePaths[index];
  457. const pattern = normalizeToPosix(patterns[index]);
  458. if (stat) {
  459. // files are added directly to the list
  460. if (stat.isFile()) {
  461. results.push(filePath);
  462. }
  463. // directories need extensions attached
  464. if (stat.isDirectory()) {
  465. if (!searches.has(filePath)) {
  466. searches.set(filePath, { patterns: [], rawPatterns: [] });
  467. }
  468. ({ patterns: globbyPatterns, rawPatterns } = searches.get(filePath));
  469. globbyPatterns.push(`${normalizeToPosix(filePath)}/**`);
  470. rawPatterns.push(pattern);
  471. }
  472. return;
  473. }
  474. // save patterns for later use based on whether globs are enabled
  475. if (globInputPaths && isGlobPattern(pattern)) {
  476. /*
  477. * We are grouping patterns by their glob parent. This is done to
  478. * make it easier to determine when a config file should be loaded.
  479. */
  480. const basePath = path.resolve(cwd, globParent(pattern));
  481. if (!searches.has(basePath)) {
  482. searches.set(basePath, { patterns: [], rawPatterns: [] });
  483. }
  484. ({ patterns: globbyPatterns, rawPatterns } = searches.get(basePath));
  485. globbyPatterns.push(filePath);
  486. rawPatterns.push(pattern);
  487. } else {
  488. missingPatterns.push(pattern);
  489. }
  490. });
  491. // there were patterns that didn't match anything, tell the user
  492. if (errorOnUnmatchedPattern && missingPatterns.length) {
  493. throw new NoFilesFoundError(missingPatterns[0], globInputPaths);
  494. }
  495. // now we are safe to do the search
  496. const globbyResults = await globMultiSearch({
  497. searches,
  498. configLoader,
  499. errorOnUnmatchedPattern
  500. });
  501. return [
  502. ...new Set([
  503. ...results,
  504. ...globbyResults
  505. ])
  506. ];
  507. }
  508. //-----------------------------------------------------------------------------
  509. // Results-related Helpers
  510. //-----------------------------------------------------------------------------
  511. /**
  512. * Checks if the given message is an error message.
  513. * @param {LintMessage} message The message to check.
  514. * @returns {boolean} Whether or not the message is an error message.
  515. * @private
  516. */
  517. function isErrorMessage(message) {
  518. return message.severity === 2;
  519. }
  520. /**
  521. * Returns result with warning by ignore settings
  522. * @param {string} filePath Absolute file path of checked code
  523. * @param {string} baseDir Absolute path of base directory
  524. * @param {"ignored"|"external"|"unconfigured"} configStatus A status that determines why the file is ignored
  525. * @returns {LintResult} Result with single warning
  526. * @private
  527. */
  528. function createIgnoreResult(filePath, baseDir, configStatus) {
  529. let message;
  530. switch (configStatus) {
  531. case "external":
  532. message = "File ignored because outside of base path.";
  533. break;
  534. case "unconfigured":
  535. message = "File ignored because no matching configuration was supplied.";
  536. break;
  537. default:
  538. {
  539. const isInNodeModules = baseDir && path.dirname(path.relative(baseDir, filePath)).split(path.sep).includes("node_modules");
  540. if (isInNodeModules) {
  541. message = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning.";
  542. } else {
  543. message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning.";
  544. }
  545. }
  546. break;
  547. }
  548. return {
  549. filePath,
  550. messages: [
  551. {
  552. ruleId: null,
  553. fatal: false,
  554. severity: 1,
  555. message,
  556. nodeType: null
  557. }
  558. ],
  559. suppressedMessages: [],
  560. errorCount: 0,
  561. warningCount: 1,
  562. fatalErrorCount: 0,
  563. fixableErrorCount: 0,
  564. fixableWarningCount: 0
  565. };
  566. }
  567. //-----------------------------------------------------------------------------
  568. // Options-related Helpers
  569. //-----------------------------------------------------------------------------
  570. /**
  571. * Check if a given value is a valid fix type or not.
  572. * @param {any} x The value to check.
  573. * @returns {boolean} `true` if `x` is valid fix type.
  574. */
  575. function isFixType(x) {
  576. return x === "directive" || x === "problem" || x === "suggestion" || x === "layout";
  577. }
  578. /**
  579. * Check if a given value is an array of fix types or not.
  580. * @param {any} x The value to check.
  581. * @returns {boolean} `true` if `x` is an array of fix types.
  582. */
  583. function isFixTypeArray(x) {
  584. return Array.isArray(x) && x.every(isFixType);
  585. }
  586. /**
  587. * The error for invalid options.
  588. */
  589. class ESLintInvalidOptionsError extends Error {
  590. constructor(messages) {
  591. super(`Invalid Options:\n- ${messages.join("\n- ")}`);
  592. this.code = "ESLINT_INVALID_OPTIONS";
  593. Error.captureStackTrace(this, ESLintInvalidOptionsError);
  594. }
  595. }
  596. /**
  597. * Validates and normalizes options for the wrapped CLIEngine instance.
  598. * @param {ESLintOptions} options The options to process.
  599. * @throws {ESLintInvalidOptionsError} If of any of a variety of type errors.
  600. * @returns {ESLintOptions} The normalized options.
  601. */
  602. function processOptions({
  603. allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instead because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored.
  604. baseConfig = null,
  605. cache = false,
  606. cacheLocation = ".eslintcache",
  607. cacheStrategy = "metadata",
  608. cwd = process.cwd(),
  609. errorOnUnmatchedPattern = true,
  610. fix = false,
  611. 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.
  612. flags = [],
  613. globInputPaths = true,
  614. ignore = true,
  615. ignorePatterns = null,
  616. overrideConfig = null,
  617. overrideConfigFile = null,
  618. plugins = {},
  619. stats = false,
  620. warnIgnored = true,
  621. passOnNoPatterns = false,
  622. ruleFilter = () => true,
  623. ...unknownOptions
  624. }) {
  625. const errors = [];
  626. const unknownOptionKeys = Object.keys(unknownOptions);
  627. if (unknownOptionKeys.length >= 1) {
  628. errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`);
  629. if (unknownOptionKeys.includes("cacheFile")) {
  630. errors.push("'cacheFile' has been removed. Please use the 'cacheLocation' option instead.");
  631. }
  632. if (unknownOptionKeys.includes("configFile")) {
  633. errors.push("'configFile' has been removed. Please use the 'overrideConfigFile' option instead.");
  634. }
  635. if (unknownOptionKeys.includes("envs")) {
  636. errors.push("'envs' has been removed.");
  637. }
  638. if (unknownOptionKeys.includes("extensions")) {
  639. errors.push("'extensions' has been removed.");
  640. }
  641. if (unknownOptionKeys.includes("resolvePluginsRelativeTo")) {
  642. errors.push("'resolvePluginsRelativeTo' has been removed.");
  643. }
  644. if (unknownOptionKeys.includes("globals")) {
  645. errors.push("'globals' has been removed. Please use the 'overrideConfig.languageOptions.globals' option instead.");
  646. }
  647. if (unknownOptionKeys.includes("ignorePath")) {
  648. errors.push("'ignorePath' has been removed.");
  649. }
  650. if (unknownOptionKeys.includes("ignorePattern")) {
  651. errors.push("'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead.");
  652. }
  653. if (unknownOptionKeys.includes("parser")) {
  654. errors.push("'parser' has been removed. Please use the 'overrideConfig.languageOptions.parser' option instead.");
  655. }
  656. if (unknownOptionKeys.includes("parserOptions")) {
  657. errors.push("'parserOptions' has been removed. Please use the 'overrideConfig.languageOptions.parserOptions' option instead.");
  658. }
  659. if (unknownOptionKeys.includes("rules")) {
  660. errors.push("'rules' has been removed. Please use the 'overrideConfig.rules' option instead.");
  661. }
  662. if (unknownOptionKeys.includes("rulePaths")) {
  663. errors.push("'rulePaths' has been removed. Please define your rules using plugins.");
  664. }
  665. if (unknownOptionKeys.includes("reportUnusedDisableDirectives")) {
  666. errors.push("'reportUnusedDisableDirectives' has been removed. Please use the 'overrideConfig.linterOptions.reportUnusedDisableDirectives' option instead.");
  667. }
  668. }
  669. if (typeof allowInlineConfig !== "boolean") {
  670. errors.push("'allowInlineConfig' must be a boolean.");
  671. }
  672. if (typeof baseConfig !== "object") {
  673. errors.push("'baseConfig' must be an object or null.");
  674. }
  675. if (typeof cache !== "boolean") {
  676. errors.push("'cache' must be a boolean.");
  677. }
  678. if (!isNonEmptyString(cacheLocation)) {
  679. errors.push("'cacheLocation' must be a non-empty string.");
  680. }
  681. if (
  682. cacheStrategy !== "metadata" &&
  683. cacheStrategy !== "content"
  684. ) {
  685. errors.push("'cacheStrategy' must be any of \"metadata\", \"content\".");
  686. }
  687. if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) {
  688. errors.push("'cwd' must be an absolute path.");
  689. }
  690. if (typeof errorOnUnmatchedPattern !== "boolean") {
  691. errors.push("'errorOnUnmatchedPattern' must be a boolean.");
  692. }
  693. if (typeof fix !== "boolean" && typeof fix !== "function") {
  694. errors.push("'fix' must be a boolean or a function.");
  695. }
  696. if (fixTypes !== null && !isFixTypeArray(fixTypes)) {
  697. errors.push("'fixTypes' must be an array of any of \"directive\", \"problem\", \"suggestion\", and \"layout\".");
  698. }
  699. if (!isEmptyArrayOrArrayOfNonEmptyString(flags)) {
  700. errors.push("'flags' must be an array of non-empty strings.");
  701. }
  702. if (typeof globInputPaths !== "boolean") {
  703. errors.push("'globInputPaths' must be a boolean.");
  704. }
  705. if (typeof ignore !== "boolean") {
  706. errors.push("'ignore' must be a boolean.");
  707. }
  708. if (!isEmptyArrayOrArrayOfNonEmptyString(ignorePatterns) && ignorePatterns !== null) {
  709. errors.push("'ignorePatterns' must be an array of non-empty strings or null.");
  710. }
  711. if (typeof overrideConfig !== "object") {
  712. errors.push("'overrideConfig' must be an object or null.");
  713. }
  714. if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null && overrideConfigFile !== true) {
  715. errors.push("'overrideConfigFile' must be a non-empty string, null, or true.");
  716. }
  717. if (typeof passOnNoPatterns !== "boolean") {
  718. errors.push("'passOnNoPatterns' must be a boolean.");
  719. }
  720. if (typeof plugins !== "object") {
  721. errors.push("'plugins' must be an object or null.");
  722. } else if (plugins !== null && Object.keys(plugins).includes("")) {
  723. errors.push("'plugins' must not include an empty string.");
  724. }
  725. if (Array.isArray(plugins)) {
  726. errors.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead.");
  727. }
  728. if (typeof stats !== "boolean") {
  729. errors.push("'stats' must be a boolean.");
  730. }
  731. if (typeof warnIgnored !== "boolean") {
  732. errors.push("'warnIgnored' must be a boolean.");
  733. }
  734. if (typeof ruleFilter !== "function") {
  735. errors.push("'ruleFilter' must be a function.");
  736. }
  737. if (errors.length > 0) {
  738. throw new ESLintInvalidOptionsError(errors);
  739. }
  740. return {
  741. allowInlineConfig,
  742. baseConfig,
  743. cache,
  744. cacheLocation,
  745. cacheStrategy,
  746. // when overrideConfigFile is true that means don't do config file lookup
  747. configFile: overrideConfigFile === true ? false : overrideConfigFile,
  748. overrideConfig,
  749. cwd: path.normalize(cwd),
  750. errorOnUnmatchedPattern,
  751. fix,
  752. fixTypes,
  753. flags: [...flags],
  754. globInputPaths,
  755. ignore,
  756. ignorePatterns,
  757. stats,
  758. passOnNoPatterns,
  759. warnIgnored,
  760. ruleFilter
  761. };
  762. }
  763. //-----------------------------------------------------------------------------
  764. // Cache-related helpers
  765. //-----------------------------------------------------------------------------
  766. /**
  767. * return the cacheFile to be used by eslint, based on whether the provided parameter is
  768. * a directory or looks like a directory (ends in `path.sep`), in which case the file
  769. * name will be the `cacheFile/.cache_hashOfCWD`
  770. *
  771. * if cacheFile points to a file or looks like a file then in will just use that file
  772. * @param {string} cacheFile The name of file to be used to store the cache
  773. * @param {string} cwd Current working directory
  774. * @returns {string} the resolved path to the cache file
  775. */
  776. function getCacheFile(cacheFile, cwd) {
  777. /*
  778. * make sure the path separators are normalized for the environment/os
  779. * keeping the trailing path separator if present
  780. */
  781. const normalizedCacheFile = path.normalize(cacheFile);
  782. const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile);
  783. const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep;
  784. /**
  785. * return the name for the cache file in case the provided parameter is a directory
  786. * @returns {string} the resolved path to the cacheFile
  787. */
  788. function getCacheFileForDirectory() {
  789. return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
  790. }
  791. let fileStats;
  792. try {
  793. fileStats = fs.lstatSync(resolvedCacheFile);
  794. } catch {
  795. fileStats = null;
  796. }
  797. /*
  798. * in case the file exists we need to verify if the provided path
  799. * is a directory or a file. If it is a directory we want to create a file
  800. * inside that directory
  801. */
  802. if (fileStats) {
  803. /*
  804. * is a directory or is a file, but the original file the user provided
  805. * looks like a directory but `path.resolve` removed the `last path.sep`
  806. * so we need to still treat this like a directory
  807. */
  808. if (fileStats.isDirectory() || looksLikeADirectory) {
  809. return getCacheFileForDirectory();
  810. }
  811. // is file so just use that file
  812. return resolvedCacheFile;
  813. }
  814. /*
  815. * here we known the file or directory doesn't exist,
  816. * so we will try to infer if its a directory if it looks like a directory
  817. * for the current operating system.
  818. */
  819. // if the last character passed is a path separator we assume is a directory
  820. if (looksLikeADirectory) {
  821. return getCacheFileForDirectory();
  822. }
  823. return resolvedCacheFile;
  824. }
  825. //-----------------------------------------------------------------------------
  826. // Exports
  827. //-----------------------------------------------------------------------------
  828. module.exports = {
  829. findFiles,
  830. isNonEmptyString,
  831. isArrayOfNonEmptyString,
  832. createIgnoreResult,
  833. isErrorMessage,
  834. processOptions,
  835. getCacheFile
  836. };