eslint-helpers.js 34 KB

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