eslint-helpers.js 32 KB

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