file-enumerator.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /**
  2. * @fileoverview `FileEnumerator` class.
  3. *
  4. * `FileEnumerator` class has two responsibilities:
  5. *
  6. * 1. Find target files by processing glob patterns.
  7. * 2. Tie each target file and appropriate configuration.
  8. *
  9. * It provides a method:
  10. *
  11. * - `iterateFiles(patterns)`
  12. * Iterate files which are matched by given patterns together with the
  13. * corresponded configuration. This is for `CLIEngine#executeOnFiles()`.
  14. * While iterating files, it loads the configuration file of each directory
  15. * before iterate files on the directory, so we can use the configuration
  16. * files to determine target files.
  17. *
  18. * @example
  19. * const enumerator = new FileEnumerator();
  20. * const linter = new Linter();
  21. *
  22. * for (const { config, filePath } of enumerator.iterateFiles(["*.js"])) {
  23. * const code = fs.readFileSync(filePath, "utf8");
  24. * const messages = linter.verify(code, config, filePath);
  25. *
  26. * console.log(messages);
  27. * }
  28. *
  29. * @author Toru Nagashima <https://github.com/mysticatea>
  30. */
  31. "use strict";
  32. //------------------------------------------------------------------------------
  33. // Requirements
  34. //------------------------------------------------------------------------------
  35. const fs = require("fs");
  36. const path = require("path");
  37. const getGlobParent = require("glob-parent");
  38. const isGlob = require("is-glob");
  39. const { escapeRegExp } = require("lodash");
  40. const { Minimatch } = require("minimatch");
  41. const { IgnorePattern } = require("./config-array");
  42. const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory");
  43. const debug = require("debug")("eslint:file-enumerator");
  44. //------------------------------------------------------------------------------
  45. // Helpers
  46. //------------------------------------------------------------------------------
  47. const minimatchOpts = { dot: true, matchBase: true };
  48. const dotfilesPattern = /(?:(?:^\.)|(?:[/\\]\.))[^/\\.].*/u;
  49. const NONE = 0;
  50. const IGNORED_SILENTLY = 1;
  51. const IGNORED = 2;
  52. // For VSCode intellisense
  53. /** @typedef {ReturnType<CascadingConfigArrayFactory["getConfigArrayForFile"]>} ConfigArray */
  54. /**
  55. * @typedef {Object} FileEnumeratorOptions
  56. * @property {CascadingConfigArrayFactory} [configArrayFactory] The factory for config arrays.
  57. * @property {string} [cwd] The base directory to start lookup.
  58. * @property {string[]} [extensions] The extensions to match files for directory patterns.
  59. * @property {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file.
  60. * @property {boolean} [ignore] The flag to check ignored files.
  61. * @property {string[]} [rulePaths] The value of `--rulesdir` option.
  62. */
  63. /**
  64. * @typedef {Object} FileAndConfig
  65. * @property {string} filePath The path to a target file.
  66. * @property {ConfigArray} config The config entries of that file.
  67. * @property {boolean} ignored If `true` then this file should be ignored and warned because it was directly specified.
  68. */
  69. /**
  70. * @typedef {Object} FileEntry
  71. * @property {string} filePath The path to a target file.
  72. * @property {ConfigArray} config The config entries of that file.
  73. * @property {NONE|IGNORED_SILENTLY|IGNORED} flag The flag.
  74. * - `NONE` means the file is a target file.
  75. * - `IGNORED_SILENTLY` means the file should be ignored silently.
  76. * - `IGNORED` means the file should be ignored and warned because it was directly specified.
  77. */
  78. /**
  79. * @typedef {Object} FileEnumeratorInternalSlots
  80. * @property {CascadingConfigArrayFactory} configArrayFactory The factory for config arrays.
  81. * @property {string} cwd The base directory to start lookup.
  82. * @property {RegExp|null} extensionRegExp The RegExp to test if a string ends with specific file extensions.
  83. * @property {boolean} globInputPaths Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file.
  84. * @property {boolean} ignoreFlag The flag to check ignored files.
  85. * @property {(filePath:string, dot:boolean) => boolean} defaultIgnores The default predicate function to ignore files.
  86. */
  87. /** @type {WeakMap<FileEnumerator, FileEnumeratorInternalSlots>} */
  88. const internalSlotsMap = new WeakMap();
  89. /**
  90. * Check if a string is a glob pattern or not.
  91. * @param {string} pattern A glob pattern.
  92. * @returns {boolean} `true` if the string is a glob pattern.
  93. */
  94. function isGlobPattern(pattern) {
  95. return isGlob(path.sep === "\\" ? pattern.replace(/\\/gu, "/") : pattern);
  96. }
  97. /**
  98. * Get stats of a given path.
  99. * @param {string} filePath The path to target file.
  100. * @returns {fs.Stats|null} The stats.
  101. * @private
  102. */
  103. function statSafeSync(filePath) {
  104. try {
  105. return fs.statSync(filePath);
  106. } catch (error) {
  107. /* istanbul ignore next */
  108. if (error.code !== "ENOENT") {
  109. throw error;
  110. }
  111. return null;
  112. }
  113. }
  114. /**
  115. * Get filenames in a given path to a directory.
  116. * @param {string} directoryPath The path to target directory.
  117. * @returns {import("fs").Dirent[]} The filenames.
  118. * @private
  119. */
  120. function readdirSafeSync(directoryPath) {
  121. try {
  122. return fs.readdirSync(directoryPath, { withFileTypes: true });
  123. } catch (error) {
  124. /* istanbul ignore next */
  125. if (error.code !== "ENOENT") {
  126. throw error;
  127. }
  128. return [];
  129. }
  130. }
  131. /**
  132. * Create a `RegExp` object to detect extensions.
  133. * @param {string[] | null} extensions The extensions to create.
  134. * @returns {RegExp | null} The created `RegExp` object or null.
  135. */
  136. function createExtensionRegExp(extensions) {
  137. if (extensions) {
  138. const normalizedExts = extensions.map(ext => escapeRegExp(
  139. ext.startsWith(".")
  140. ? ext.slice(1)
  141. : ext
  142. ));
  143. return new RegExp(
  144. `.\\.(?:${normalizedExts.join("|")})$`,
  145. "u"
  146. );
  147. }
  148. return null;
  149. }
  150. /**
  151. * The error type when no files match a glob.
  152. */
  153. class NoFilesFoundError extends Error {
  154. // eslint-disable-next-line jsdoc/require-description
  155. /**
  156. * @param {string} pattern The glob pattern which was not found.
  157. * @param {boolean} globDisabled If `true` then the pattern was a glob pattern, but glob was disabled.
  158. */
  159. constructor(pattern, globDisabled) {
  160. super(`No files matching '${pattern}' were found${globDisabled ? " (glob was disabled)" : ""}.`);
  161. this.messageTemplate = "file-not-found";
  162. this.messageData = { pattern, globDisabled };
  163. }
  164. }
  165. /**
  166. * The error type when there are files matched by a glob, but all of them have been ignored.
  167. */
  168. class AllFilesIgnoredError extends Error {
  169. // eslint-disable-next-line jsdoc/require-description
  170. /**
  171. * @param {string} pattern The glob pattern which was not found.
  172. */
  173. constructor(pattern) {
  174. super(`All files matched by '${pattern}' are ignored.`);
  175. this.messageTemplate = "all-files-ignored";
  176. this.messageData = { pattern };
  177. }
  178. }
  179. /**
  180. * This class provides the functionality that enumerates every file which is
  181. * matched by given glob patterns and that configuration.
  182. */
  183. class FileEnumerator {
  184. /**
  185. * Initialize this enumerator.
  186. * @param {FileEnumeratorOptions} options The options.
  187. */
  188. constructor({
  189. cwd = process.cwd(),
  190. configArrayFactory = new CascadingConfigArrayFactory({ cwd }),
  191. extensions = null,
  192. globInputPaths = true,
  193. errorOnUnmatchedPattern = true,
  194. ignore = true
  195. } = {}) {
  196. internalSlotsMap.set(this, {
  197. configArrayFactory,
  198. cwd,
  199. defaultIgnores: IgnorePattern.createDefaultIgnore(cwd),
  200. extensionRegExp: createExtensionRegExp(extensions),
  201. globInputPaths,
  202. errorOnUnmatchedPattern,
  203. ignoreFlag: ignore
  204. });
  205. }
  206. /**
  207. * Check if a given file is target or not.
  208. * @param {string} filePath The path to a candidate file.
  209. * @param {ConfigArray} [providedConfig] Optional. The configuration for the file.
  210. * @returns {boolean} `true` if the file is a target.
  211. */
  212. isTargetPath(filePath, providedConfig) {
  213. const {
  214. configArrayFactory,
  215. extensionRegExp
  216. } = internalSlotsMap.get(this);
  217. // If `--ext` option is present, use it.
  218. if (extensionRegExp) {
  219. return extensionRegExp.test(filePath);
  220. }
  221. // `.js` file is target by default.
  222. if (filePath.endsWith(".js")) {
  223. return true;
  224. }
  225. // use `overrides[].files` to check additional targets.
  226. const config =
  227. providedConfig ||
  228. configArrayFactory.getConfigArrayForFile(
  229. filePath,
  230. { ignoreNotFoundError: true }
  231. );
  232. return config.isAdditionalTargetPath(filePath);
  233. }
  234. /**
  235. * Iterate files which are matched by given glob patterns.
  236. * @param {string|string[]} patternOrPatterns The glob patterns to iterate files.
  237. * @returns {IterableIterator<FileAndConfig>} The found files.
  238. */
  239. *iterateFiles(patternOrPatterns) {
  240. const { globInputPaths, errorOnUnmatchedPattern } = internalSlotsMap.get(this);
  241. const patterns = Array.isArray(patternOrPatterns)
  242. ? patternOrPatterns
  243. : [patternOrPatterns];
  244. debug("Start to iterate files: %o", patterns);
  245. // The set of paths to remove duplicate.
  246. const set = new Set();
  247. for (const pattern of patterns) {
  248. let foundRegardlessOfIgnored = false;
  249. let found = false;
  250. // Skip empty string.
  251. if (!pattern) {
  252. continue;
  253. }
  254. // Iterate files of this pattern.
  255. for (const { config, filePath, flag } of this._iterateFiles(pattern)) {
  256. foundRegardlessOfIgnored = true;
  257. if (flag === IGNORED_SILENTLY) {
  258. continue;
  259. }
  260. found = true;
  261. // Remove duplicate paths while yielding paths.
  262. if (!set.has(filePath)) {
  263. set.add(filePath);
  264. yield {
  265. config,
  266. filePath,
  267. ignored: flag === IGNORED
  268. };
  269. }
  270. }
  271. // Raise an error if any files were not found.
  272. if (errorOnUnmatchedPattern) {
  273. if (!foundRegardlessOfIgnored) {
  274. throw new NoFilesFoundError(
  275. pattern,
  276. !globInputPaths && isGlob(pattern)
  277. );
  278. }
  279. if (!found) {
  280. throw new AllFilesIgnoredError(pattern);
  281. }
  282. }
  283. }
  284. debug(`Complete iterating files: ${JSON.stringify(patterns)}`);
  285. }
  286. /**
  287. * Iterate files which are matched by a given glob pattern.
  288. * @param {string} pattern The glob pattern to iterate files.
  289. * @returns {IterableIterator<FileEntry>} The found files.
  290. */
  291. _iterateFiles(pattern) {
  292. const { cwd, globInputPaths } = internalSlotsMap.get(this);
  293. const absolutePath = path.resolve(cwd, pattern);
  294. const isDot = dotfilesPattern.test(pattern);
  295. const stat = statSafeSync(absolutePath);
  296. if (stat && stat.isDirectory()) {
  297. return this._iterateFilesWithDirectory(absolutePath, isDot);
  298. }
  299. if (stat && stat.isFile()) {
  300. return this._iterateFilesWithFile(absolutePath);
  301. }
  302. if (globInputPaths && isGlobPattern(pattern)) {
  303. return this._iterateFilesWithGlob(absolutePath, isDot);
  304. }
  305. return [];
  306. }
  307. /**
  308. * Iterate a file which is matched by a given path.
  309. * @param {string} filePath The path to the target file.
  310. * @returns {IterableIterator<FileEntry>} The found files.
  311. * @private
  312. */
  313. _iterateFilesWithFile(filePath) {
  314. debug(`File: ${filePath}`);
  315. const { configArrayFactory } = internalSlotsMap.get(this);
  316. const config = configArrayFactory.getConfigArrayForFile(filePath);
  317. const ignored = this._isIgnoredFile(filePath, { config, direct: true });
  318. const flag = ignored ? IGNORED : NONE;
  319. return [{ config, filePath, flag }];
  320. }
  321. /**
  322. * Iterate files in a given path.
  323. * @param {string} directoryPath The path to the target directory.
  324. * @param {boolean} dotfiles If `true` then it doesn't skip dot files by default.
  325. * @returns {IterableIterator<FileEntry>} The found files.
  326. * @private
  327. */
  328. _iterateFilesWithDirectory(directoryPath, dotfiles) {
  329. debug(`Directory: ${directoryPath}`);
  330. return this._iterateFilesRecursive(
  331. directoryPath,
  332. { dotfiles, recursive: true, selector: null }
  333. );
  334. }
  335. /**
  336. * Iterate files which are matched by a given glob pattern.
  337. * @param {string} pattern The glob pattern to iterate files.
  338. * @param {boolean} dotfiles If `true` then it doesn't skip dot files by default.
  339. * @returns {IterableIterator<FileEntry>} The found files.
  340. * @private
  341. */
  342. _iterateFilesWithGlob(pattern, dotfiles) {
  343. debug(`Glob: ${pattern}`);
  344. const directoryPath = path.resolve(getGlobParent(pattern));
  345. const globPart = pattern.slice(directoryPath.length + 1);
  346. /*
  347. * recursive if there are `**` or path separators in the glob part.
  348. * Otherwise, patterns such as `src/*.js`, it doesn't need recursive.
  349. */
  350. const recursive = /\*\*|\/|\\/u.test(globPart);
  351. const selector = new Minimatch(pattern, minimatchOpts);
  352. debug(`recursive? ${recursive}`);
  353. return this._iterateFilesRecursive(
  354. directoryPath,
  355. { dotfiles, recursive, selector }
  356. );
  357. }
  358. /**
  359. * Iterate files in a given path.
  360. * @param {string} directoryPath The path to the target directory.
  361. * @param {Object} options The options to iterate files.
  362. * @param {boolean} [options.dotfiles] If `true` then it doesn't skip dot files by default.
  363. * @param {boolean} [options.recursive] If `true` then it dives into sub directories.
  364. * @param {InstanceType<Minimatch>} [options.selector] The matcher to choose files.
  365. * @returns {IterableIterator<FileEntry>} The found files.
  366. * @private
  367. */
  368. *_iterateFilesRecursive(directoryPath, options) {
  369. debug(`Enter the directory: ${directoryPath}`);
  370. const { configArrayFactory } = internalSlotsMap.get(this);
  371. /** @type {ConfigArray|null} */
  372. let config = null;
  373. // Enumerate the files of this directory.
  374. for (const entry of readdirSafeSync(directoryPath)) {
  375. const filePath = path.join(directoryPath, entry.name);
  376. // Check if the file is matched.
  377. if (entry.isFile()) {
  378. if (!config) {
  379. config = configArrayFactory.getConfigArrayForFile(
  380. filePath,
  381. /*
  382. * We must ignore `ConfigurationNotFoundError` at this
  383. * point because we don't know if target files exist in
  384. * this directory.
  385. */
  386. { ignoreNotFoundError: true }
  387. );
  388. }
  389. const matched = options.selector
  390. // Started with a glob pattern; choose by the pattern.
  391. ? options.selector.match(filePath)
  392. // Started with a directory path; choose by file extensions.
  393. : this.isTargetPath(filePath, config);
  394. if (matched) {
  395. const ignored = this._isIgnoredFile(filePath, { ...options, config });
  396. const flag = ignored ? IGNORED_SILENTLY : NONE;
  397. debug(`Yield: ${entry.name}${ignored ? " but ignored" : ""}`);
  398. yield {
  399. config: configArrayFactory.getConfigArrayForFile(filePath),
  400. filePath,
  401. flag
  402. };
  403. } else {
  404. debug(`Didn't match: ${entry.name}`);
  405. }
  406. // Dive into the sub directory.
  407. } else if (options.recursive && entry.isDirectory()) {
  408. if (!config) {
  409. config = configArrayFactory.getConfigArrayForFile(
  410. filePath,
  411. { ignoreNotFoundError: true }
  412. );
  413. }
  414. const ignored = this._isIgnoredFile(
  415. filePath + path.sep,
  416. { ...options, config }
  417. );
  418. if (!ignored) {
  419. yield* this._iterateFilesRecursive(filePath, options);
  420. }
  421. }
  422. }
  423. debug(`Leave the directory: ${directoryPath}`);
  424. }
  425. /**
  426. * Check if a given file should be ignored.
  427. * @param {string} filePath The path to a file to check.
  428. * @param {Object} options Options
  429. * @param {ConfigArray} [options.config] The config for this file.
  430. * @param {boolean} [options.dotfiles] If `true` then this is not ignore dot files by default.
  431. * @param {boolean} [options.direct] If `true` then this is a direct specified file.
  432. * @returns {boolean} `true` if the file should be ignored.
  433. * @private
  434. */
  435. _isIgnoredFile(filePath, {
  436. config: providedConfig,
  437. dotfiles = false,
  438. direct = false
  439. }) {
  440. const {
  441. configArrayFactory,
  442. defaultIgnores,
  443. ignoreFlag
  444. } = internalSlotsMap.get(this);
  445. if (ignoreFlag) {
  446. const config =
  447. providedConfig ||
  448. configArrayFactory.getConfigArrayForFile(
  449. filePath,
  450. { ignoreNotFoundError: true }
  451. );
  452. const ignores =
  453. config.extractConfig(filePath).ignores || defaultIgnores;
  454. return ignores(filePath, dotfiles);
  455. }
  456. return !direct && defaultIgnores(filePath, dotfiles);
  457. }
  458. }
  459. //------------------------------------------------------------------------------
  460. // Public Interface
  461. //------------------------------------------------------------------------------
  462. module.exports = { FileEnumerator };