Software zum Installieren eines Smart-Mirror Frameworks , zum Nutzen von hochschulrelevanten Informationen, auf einem Raspberry-Pi.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

file-enumerator.js 19KB

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