file-enumerator.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  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 provies 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} 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 {string[]} The filenames.
  118. * @private
  119. */
  120. function readdirSafeSync(directoryPath) {
  121. try {
  122. return fs.readdirSync(directoryPath);
  123. } catch (error) {
  124. /* istanbul ignore next */
  125. if (error.code !== "ENOENT") {
  126. throw error;
  127. }
  128. return [];
  129. }
  130. }
  131. /**
  132. * The error type when no files match a glob.
  133. */
  134. class NoFilesFoundError extends Error {
  135. // eslint-disable-next-line jsdoc/require-description
  136. /**
  137. * @param {string} pattern The glob pattern which was not found.
  138. * @param {boolean} globDisabled If `true` then the pattern was a glob pattern, but glob was disabled.
  139. */
  140. constructor(pattern, globDisabled) {
  141. super(`No files matching '${pattern}' were found${globDisabled ? " (glob was disabled)" : ""}.`);
  142. this.messageTemplate = "file-not-found";
  143. this.messageData = { pattern, globDisabled };
  144. }
  145. }
  146. /**
  147. * The error type when there are files matched by a glob, but all of them have been ignored.
  148. */
  149. class AllFilesIgnoredError extends Error {
  150. // eslint-disable-next-line jsdoc/require-description
  151. /**
  152. * @param {string} pattern The glob pattern which was not found.
  153. */
  154. constructor(pattern) {
  155. super(`All files matched by '${pattern}' are ignored.`);
  156. this.messageTemplate = "all-files-ignored";
  157. this.messageData = { pattern };
  158. }
  159. }
  160. /**
  161. * This class provides the functionality that enumerates every file which is
  162. * matched by given glob patterns and that configuration.
  163. */
  164. class FileEnumerator {
  165. /**
  166. * Initialize this enumerator.
  167. * @param {FileEnumeratorOptions} options The options.
  168. */
  169. constructor({
  170. cwd = process.cwd(),
  171. configArrayFactory = new CascadingConfigArrayFactory({ cwd }),
  172. extensions = [".js"],
  173. globInputPaths = true,
  174. errorOnUnmatchedPattern = true,
  175. ignore = true
  176. } = {}) {
  177. internalSlotsMap.set(this, {
  178. configArrayFactory,
  179. cwd,
  180. defaultIgnores: IgnorePattern.createDefaultIgnore(cwd),
  181. extensionRegExp: new RegExp(
  182. `.\\.(?:${extensions
  183. .map(ext => escapeRegExp(
  184. ext.startsWith(".")
  185. ? ext.slice(1)
  186. : ext
  187. ))
  188. .join("|")
  189. })$`,
  190. "u"
  191. ),
  192. globInputPaths,
  193. errorOnUnmatchedPattern,
  194. ignoreFlag: ignore
  195. });
  196. }
  197. /**
  198. * The `RegExp` object that tests if a file path has the allowed file extensions.
  199. * @type {RegExp}
  200. */
  201. get extensionRegExp() {
  202. return internalSlotsMap.get(this).extensionRegExp;
  203. }
  204. /**
  205. * Iterate files which are matched by given glob patterns.
  206. * @param {string|string[]} patternOrPatterns The glob patterns to iterate files.
  207. * @returns {IterableIterator<FileAndConfig>} The found files.
  208. */
  209. *iterateFiles(patternOrPatterns) {
  210. const { globInputPaths, errorOnUnmatchedPattern } = internalSlotsMap.get(this);
  211. const patterns = Array.isArray(patternOrPatterns)
  212. ? patternOrPatterns
  213. : [patternOrPatterns];
  214. debug("Start to iterate files: %o", patterns);
  215. // The set of paths to remove duplicate.
  216. const set = new Set();
  217. for (const pattern of patterns) {
  218. let foundRegardlessOfIgnored = false;
  219. let found = false;
  220. // Skip empty string.
  221. if (!pattern) {
  222. continue;
  223. }
  224. // Iterate files of this pttern.
  225. for (const { config, filePath, flag } of this._iterateFiles(pattern)) {
  226. foundRegardlessOfIgnored = true;
  227. if (flag === IGNORED_SILENTLY) {
  228. continue;
  229. }
  230. found = true;
  231. // Remove duplicate paths while yielding paths.
  232. if (!set.has(filePath)) {
  233. set.add(filePath);
  234. yield {
  235. config,
  236. filePath,
  237. ignored: flag === IGNORED
  238. };
  239. }
  240. }
  241. // Raise an error if any files were not found.
  242. if (errorOnUnmatchedPattern) {
  243. if (!foundRegardlessOfIgnored) {
  244. throw new NoFilesFoundError(
  245. pattern,
  246. !globInputPaths && isGlob(pattern)
  247. );
  248. }
  249. if (!found) {
  250. throw new AllFilesIgnoredError(pattern);
  251. }
  252. }
  253. }
  254. debug(`Complete iterating files: ${JSON.stringify(patterns)}`);
  255. }
  256. /**
  257. * Iterate files which are matched by a given glob pattern.
  258. * @param {string} pattern The glob pattern to iterate files.
  259. * @returns {IterableIterator<FileEntry>} The found files.
  260. */
  261. _iterateFiles(pattern) {
  262. const { cwd, globInputPaths } = internalSlotsMap.get(this);
  263. const absolutePath = path.resolve(cwd, pattern);
  264. const isDot = dotfilesPattern.test(pattern);
  265. const stat = statSafeSync(absolutePath);
  266. if (stat && stat.isDirectory()) {
  267. return this._iterateFilesWithDirectory(absolutePath, isDot);
  268. }
  269. if (stat && stat.isFile()) {
  270. return this._iterateFilesWithFile(absolutePath);
  271. }
  272. if (globInputPaths && isGlobPattern(pattern)) {
  273. return this._iterateFilesWithGlob(absolutePath, isDot);
  274. }
  275. return [];
  276. }
  277. /**
  278. * Iterate a file which is matched by a given path.
  279. * @param {string} filePath The path to the target file.
  280. * @returns {IterableIterator<FileEntry>} The found files.
  281. * @private
  282. */
  283. _iterateFilesWithFile(filePath) {
  284. debug(`File: ${filePath}`);
  285. const { configArrayFactory } = internalSlotsMap.get(this);
  286. const config = configArrayFactory.getConfigArrayForFile(filePath);
  287. const ignored = this._isIgnoredFile(filePath, { config, direct: true });
  288. const flag = ignored ? IGNORED : NONE;
  289. return [{ config, filePath, flag }];
  290. }
  291. /**
  292. * Iterate files in a given path.
  293. * @param {string} directoryPath The path to the target directory.
  294. * @param {boolean} dotfiles If `true` then it doesn't skip dot files by default.
  295. * @returns {IterableIterator<FileEntry>} The found files.
  296. * @private
  297. */
  298. _iterateFilesWithDirectory(directoryPath, dotfiles) {
  299. debug(`Directory: ${directoryPath}`);
  300. return this._iterateFilesRecursive(
  301. directoryPath,
  302. { dotfiles, recursive: true, selector: null }
  303. );
  304. }
  305. /**
  306. * Iterate files which are matched by a given glob pattern.
  307. * @param {string} pattern The glob pattern to iterate files.
  308. * @param {boolean} dotfiles If `true` then it doesn't skip dot files by default.
  309. * @returns {IterableIterator<FileEntry>} The found files.
  310. * @private
  311. */
  312. _iterateFilesWithGlob(pattern, dotfiles) {
  313. debug(`Glob: ${pattern}`);
  314. const directoryPath = path.resolve(getGlobParent(pattern));
  315. const globPart = pattern.slice(directoryPath.length + 1);
  316. /*
  317. * recursive if there are `**` or path separators in the glob part.
  318. * Otherwise, patterns such as `src/*.js`, it doesn't need recursive.
  319. */
  320. const recursive = /\*\*|\/|\\/u.test(globPart);
  321. const selector = new Minimatch(pattern, minimatchOpts);
  322. debug(`recursive? ${recursive}`);
  323. return this._iterateFilesRecursive(
  324. directoryPath,
  325. { dotfiles, recursive, selector }
  326. );
  327. }
  328. /**
  329. * Iterate files in a given path.
  330. * @param {string} directoryPath The path to the target directory.
  331. * @param {Object} options The options to iterate files.
  332. * @param {boolean} [options.dotfiles] If `true` then it doesn't skip dot files by default.
  333. * @param {boolean} [options.recursive] If `true` then it dives into sub directories.
  334. * @param {InstanceType<Minimatch>} [options.selector] The matcher to choose files.
  335. * @returns {IterableIterator<FileEntry>} The found files.
  336. * @private
  337. */
  338. *_iterateFilesRecursive(directoryPath, options) {
  339. debug(`Enter the directory: ${directoryPath}`);
  340. const { configArrayFactory, extensionRegExp } = internalSlotsMap.get(this);
  341. /** @type {ConfigArray|null} */
  342. let config = null;
  343. // Enumerate the files of this directory.
  344. for (const filename of readdirSafeSync(directoryPath)) {
  345. const filePath = path.join(directoryPath, filename);
  346. const stat = statSafeSync(filePath); // TODO: Use `withFileTypes` in the future.
  347. // Check if the file is matched.
  348. if (stat && stat.isFile()) {
  349. if (!config) {
  350. config = configArrayFactory.getConfigArrayForFile(
  351. filePath,
  352. /*
  353. * We must ignore `ConfigurationNotFoundError` at this
  354. * point because we don't know if target files exist in
  355. * this directory.
  356. */
  357. { ignoreNotFoundError: true }
  358. );
  359. }
  360. const ignored = this._isIgnoredFile(filePath, { ...options, config });
  361. const flag = ignored ? IGNORED_SILENTLY : NONE;
  362. const matched = options.selector
  363. // Started with a glob pattern; choose by the pattern.
  364. ? options.selector.match(filePath)
  365. // Started with a directory path; choose by file extensions.
  366. : extensionRegExp.test(filePath);
  367. if (matched) {
  368. debug(`Yield: ${filename}${ignored ? " but ignored" : ""}`);
  369. yield {
  370. config: configArrayFactory.getConfigArrayForFile(filePath),
  371. filePath,
  372. flag
  373. };
  374. } else {
  375. debug(`Didn't match: ${filename}`);
  376. }
  377. // Dive into the sub directory.
  378. } else if (options.recursive && stat && stat.isDirectory()) {
  379. if (!config) {
  380. config = configArrayFactory.getConfigArrayForFile(
  381. filePath,
  382. { ignoreNotFoundError: true }
  383. );
  384. }
  385. const ignored = this._isIgnoredFile(
  386. filePath + path.sep,
  387. { ...options, config }
  388. );
  389. if (!ignored) {
  390. yield* this._iterateFilesRecursive(filePath, options);
  391. }
  392. }
  393. }
  394. debug(`Leave the directory: ${directoryPath}`);
  395. }
  396. /**
  397. * Check if a given file should be ignored.
  398. * @param {string} filePath The path to a file to check.
  399. * @param {Object} options Options
  400. * @param {ConfigArray} [options.config] The config for this file.
  401. * @param {boolean} [options.dotfiles] If `true` then this is not ignore dot files by default.
  402. * @param {boolean} [options.direct] If `true` then this is a direct specified file.
  403. * @returns {boolean} `true` if the file should be ignored.
  404. * @private
  405. */
  406. _isIgnoredFile(filePath, {
  407. config: providedConfig,
  408. dotfiles = false,
  409. direct = false
  410. }) {
  411. const {
  412. configArrayFactory,
  413. defaultIgnores,
  414. ignoreFlag
  415. } = internalSlotsMap.get(this);
  416. if (ignoreFlag) {
  417. const config =
  418. providedConfig ||
  419. configArrayFactory.getConfigArrayForFile(
  420. filePath,
  421. { ignoreNotFoundError: true }
  422. );
  423. const ignores =
  424. config.extractConfig(filePath).ignores || defaultIgnores;
  425. return ignores(filePath, dotfiles);
  426. }
  427. return !direct && defaultIgnores(filePath, dotfiles);
  428. }
  429. }
  430. //------------------------------------------------------------------------------
  431. // Public Interface
  432. //------------------------------------------------------------------------------
  433. module.exports = { FileEnumerator };