eslint-helpers.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  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 {UnmatchedSearchPatternsError} 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(pattern)) {
  441. const basePath = path.resolve(cwd, globParent(pattern));
  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. // Results-related Helpers
  477. //-----------------------------------------------------------------------------
  478. /**
  479. * Checks if the given message is an error message.
  480. * @param {LintMessage} message The message to check.
  481. * @returns {boolean} Whether or not the message is an error message.
  482. * @private
  483. */
  484. function isErrorMessage(message) {
  485. return message.severity === 2;
  486. }
  487. /**
  488. * Returns result with warning by ignore settings
  489. * @param {string} filePath File path of checked code
  490. * @param {string} baseDir Absolute path of base directory
  491. * @returns {LintResult} Result with single warning
  492. * @private
  493. */
  494. function createIgnoreResult(filePath, baseDir) {
  495. let message;
  496. const isHidden = filePath.split(path.sep)
  497. .find(segment => /^\./u.test(segment));
  498. const isInNodeModules = baseDir && path.relative(baseDir, filePath).startsWith("node_modules");
  499. if (isHidden) {
  500. message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!<relative/path/to/filename>'\") to override.";
  501. } else if (isInNodeModules) {
  502. message = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override.";
  503. } else {
  504. message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.";
  505. }
  506. return {
  507. filePath: path.resolve(filePath),
  508. messages: [
  509. {
  510. fatal: false,
  511. severity: 1,
  512. message
  513. }
  514. ],
  515. suppressedMessages: [],
  516. errorCount: 0,
  517. warningCount: 1,
  518. fatalErrorCount: 0,
  519. fixableErrorCount: 0,
  520. fixableWarningCount: 0
  521. };
  522. }
  523. //-----------------------------------------------------------------------------
  524. // Options-related Helpers
  525. //-----------------------------------------------------------------------------
  526. /**
  527. * Check if a given value is a valid fix type or not.
  528. * @param {any} x The value to check.
  529. * @returns {boolean} `true` if `x` is valid fix type.
  530. */
  531. function isFixType(x) {
  532. return x === "directive" || x === "problem" || x === "suggestion" || x === "layout";
  533. }
  534. /**
  535. * Check if a given value is an array of fix types or not.
  536. * @param {any} x The value to check.
  537. * @returns {boolean} `true` if `x` is an array of fix types.
  538. */
  539. function isFixTypeArray(x) {
  540. return Array.isArray(x) && x.every(isFixType);
  541. }
  542. /**
  543. * The error for invalid options.
  544. */
  545. class ESLintInvalidOptionsError extends Error {
  546. constructor(messages) {
  547. super(`Invalid Options:\n- ${messages.join("\n- ")}`);
  548. this.code = "ESLINT_INVALID_OPTIONS";
  549. Error.captureStackTrace(this, ESLintInvalidOptionsError);
  550. }
  551. }
  552. /**
  553. * Validates and normalizes options for the wrapped CLIEngine instance.
  554. * @param {FlatESLintOptions} options The options to process.
  555. * @throws {ESLintInvalidOptionsError} If of any of a variety of type errors.
  556. * @returns {FlatESLintOptions} The normalized options.
  557. */
  558. function processOptions({
  559. allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instead because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored.
  560. baseConfig = null,
  561. cache = false,
  562. cacheLocation = ".eslintcache",
  563. cacheStrategy = "metadata",
  564. cwd = process.cwd(),
  565. errorOnUnmatchedPattern = true,
  566. fix = false,
  567. 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.
  568. globInputPaths = true,
  569. ignore = true,
  570. ignorePatterns = null,
  571. overrideConfig = null,
  572. overrideConfigFile = null,
  573. plugins = {},
  574. 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.
  575. ...unknownOptions
  576. }) {
  577. const errors = [];
  578. const unknownOptionKeys = Object.keys(unknownOptions);
  579. if (unknownOptionKeys.length >= 1) {
  580. errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`);
  581. if (unknownOptionKeys.includes("cacheFile")) {
  582. errors.push("'cacheFile' has been removed. Please use the 'cacheLocation' option instead.");
  583. }
  584. if (unknownOptionKeys.includes("configFile")) {
  585. errors.push("'configFile' has been removed. Please use the 'overrideConfigFile' option instead.");
  586. }
  587. if (unknownOptionKeys.includes("envs")) {
  588. errors.push("'envs' has been removed.");
  589. }
  590. if (unknownOptionKeys.includes("extensions")) {
  591. errors.push("'extensions' has been removed.");
  592. }
  593. if (unknownOptionKeys.includes("resolvePluginsRelativeTo")) {
  594. errors.push("'resolvePluginsRelativeTo' has been removed.");
  595. }
  596. if (unknownOptionKeys.includes("globals")) {
  597. errors.push("'globals' has been removed. Please use the 'overrideConfig.languageOptions.globals' option instead.");
  598. }
  599. if (unknownOptionKeys.includes("ignorePath")) {
  600. errors.push("'ignorePath' has been removed.");
  601. }
  602. if (unknownOptionKeys.includes("ignorePattern")) {
  603. errors.push("'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead.");
  604. }
  605. if (unknownOptionKeys.includes("parser")) {
  606. errors.push("'parser' has been removed. Please use the 'overrideConfig.languageOptions.parser' option instead.");
  607. }
  608. if (unknownOptionKeys.includes("parserOptions")) {
  609. errors.push("'parserOptions' has been removed. Please use the 'overrideConfig.languageOptions.parserOptions' option instead.");
  610. }
  611. if (unknownOptionKeys.includes("rules")) {
  612. errors.push("'rules' has been removed. Please use the 'overrideConfig.rules' option instead.");
  613. }
  614. if (unknownOptionKeys.includes("rulePaths")) {
  615. errors.push("'rulePaths' has been removed. Please define your rules using plugins.");
  616. }
  617. }
  618. if (typeof allowInlineConfig !== "boolean") {
  619. errors.push("'allowInlineConfig' must be a boolean.");
  620. }
  621. if (typeof baseConfig !== "object") {
  622. errors.push("'baseConfig' must be an object or null.");
  623. }
  624. if (typeof cache !== "boolean") {
  625. errors.push("'cache' must be a boolean.");
  626. }
  627. if (!isNonEmptyString(cacheLocation)) {
  628. errors.push("'cacheLocation' must be a non-empty string.");
  629. }
  630. if (
  631. cacheStrategy !== "metadata" &&
  632. cacheStrategy !== "content"
  633. ) {
  634. errors.push("'cacheStrategy' must be any of \"metadata\", \"content\".");
  635. }
  636. if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) {
  637. errors.push("'cwd' must be an absolute path.");
  638. }
  639. if (typeof errorOnUnmatchedPattern !== "boolean") {
  640. errors.push("'errorOnUnmatchedPattern' must be a boolean.");
  641. }
  642. if (typeof fix !== "boolean" && typeof fix !== "function") {
  643. errors.push("'fix' must be a boolean or a function.");
  644. }
  645. if (fixTypes !== null && !isFixTypeArray(fixTypes)) {
  646. errors.push("'fixTypes' must be an array of any of \"directive\", \"problem\", \"suggestion\", and \"layout\".");
  647. }
  648. if (typeof globInputPaths !== "boolean") {
  649. errors.push("'globInputPaths' must be a boolean.");
  650. }
  651. if (typeof ignore !== "boolean") {
  652. errors.push("'ignore' must be a boolean.");
  653. }
  654. if (typeof overrideConfig !== "object") {
  655. errors.push("'overrideConfig' must be an object or null.");
  656. }
  657. if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null && overrideConfigFile !== true) {
  658. errors.push("'overrideConfigFile' must be a non-empty string, null, or true.");
  659. }
  660. if (typeof plugins !== "object") {
  661. errors.push("'plugins' must be an object or null.");
  662. } else if (plugins !== null && Object.keys(plugins).includes("")) {
  663. errors.push("'plugins' must not include an empty string.");
  664. }
  665. if (Array.isArray(plugins)) {
  666. errors.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead.");
  667. }
  668. if (
  669. reportUnusedDisableDirectives !== "error" &&
  670. reportUnusedDisableDirectives !== "warn" &&
  671. reportUnusedDisableDirectives !== "off" &&
  672. reportUnusedDisableDirectives !== null
  673. ) {
  674. errors.push("'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null.");
  675. }
  676. if (errors.length > 0) {
  677. throw new ESLintInvalidOptionsError(errors);
  678. }
  679. return {
  680. allowInlineConfig,
  681. baseConfig,
  682. cache,
  683. cacheLocation,
  684. cacheStrategy,
  685. // when overrideConfigFile is true that means don't do config file lookup
  686. configFile: overrideConfigFile === true ? false : overrideConfigFile,
  687. overrideConfig,
  688. cwd,
  689. errorOnUnmatchedPattern,
  690. fix,
  691. fixTypes,
  692. globInputPaths,
  693. ignore,
  694. ignorePatterns,
  695. reportUnusedDisableDirectives
  696. };
  697. }
  698. //-----------------------------------------------------------------------------
  699. // Cache-related helpers
  700. //-----------------------------------------------------------------------------
  701. /**
  702. * return the cacheFile to be used by eslint, based on whether the provided parameter is
  703. * a directory or looks like a directory (ends in `path.sep`), in which case the file
  704. * name will be the `cacheFile/.cache_hashOfCWD`
  705. *
  706. * if cacheFile points to a file or looks like a file then in will just use that file
  707. * @param {string} cacheFile The name of file to be used to store the cache
  708. * @param {string} cwd Current working directory
  709. * @returns {string} the resolved path to the cache file
  710. */
  711. function getCacheFile(cacheFile, cwd) {
  712. /*
  713. * make sure the path separators are normalized for the environment/os
  714. * keeping the trailing path separator if present
  715. */
  716. const normalizedCacheFile = path.normalize(cacheFile);
  717. const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile);
  718. const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep;
  719. /**
  720. * return the name for the cache file in case the provided parameter is a directory
  721. * @returns {string} the resolved path to the cacheFile
  722. */
  723. function getCacheFileForDirectory() {
  724. return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
  725. }
  726. let fileStats;
  727. try {
  728. fileStats = fs.lstatSync(resolvedCacheFile);
  729. } catch {
  730. fileStats = null;
  731. }
  732. /*
  733. * in case the file exists we need to verify if the provided path
  734. * is a directory or a file. If it is a directory we want to create a file
  735. * inside that directory
  736. */
  737. if (fileStats) {
  738. /*
  739. * is a directory or is a file, but the original file the user provided
  740. * looks like a directory but `path.resolve` removed the `last path.sep`
  741. * so we need to still treat this like a directory
  742. */
  743. if (fileStats.isDirectory() || looksLikeADirectory) {
  744. return getCacheFileForDirectory();
  745. }
  746. // is file so just use that file
  747. return resolvedCacheFile;
  748. }
  749. /*
  750. * here we known the file or directory doesn't exist,
  751. * so we will try to infer if its a directory if it looks like a directory
  752. * for the current operating system.
  753. */
  754. // if the last character passed is a path separator we assume is a directory
  755. if (looksLikeADirectory) {
  756. return getCacheFileForDirectory();
  757. }
  758. return resolvedCacheFile;
  759. }
  760. //-----------------------------------------------------------------------------
  761. // Exports
  762. //-----------------------------------------------------------------------------
  763. module.exports = {
  764. isGlobPattern,
  765. findFiles,
  766. isNonEmptyString,
  767. isArrayOfNonEmptyString,
  768. createIgnoreResult,
  769. isErrorMessage,
  770. processOptions,
  771. getCacheFile
  772. };