source: imaps-frontend/node_modules/eslint/lib/cli-engine/file-enumerator.js@ d565449

main
Last change on this file since d565449 was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

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