Skip to content

Commit cb90bd1

Browse files
[PoC] Created FileEnumeratorIsh to demonstrate flat config support
1 parent e55d05a commit cb90bd1

File tree

2 files changed

+240
-48
lines changed

2 files changed

+240
-48
lines changed

src/FileEnumeratorIsh.js

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
const escapeRegExp = require("escape-string-regexp");
4+
5+
const dotfilesPattern = /(?:(?:^\.)|(?:[/\\]\.))[^/\\.].*/u;
6+
const NONE = 0;
7+
const IGNORED_SILENTLY = 1;
8+
const IGNORED = 2;
9+
10+
/**
11+
* @typedef {Object} FileEnumeratorOptions
12+
* @property {CascadingConfigArrayFactory} [configArrayFactory] The factory for config arrays.
13+
* @property {string} [cwd] The base directory to start lookup.
14+
* @property {string[]} [extensions] The extensions to match files for directory patterns.
15+
* @property {(directoryPath: string) => boolean} [isDirectoryIgnored] Returns whether a directory is ignored.
16+
* @property {(filePath: string) => boolean} [isFileIgnored] Returns whether a file is ignored.
17+
*/
18+
19+
/**
20+
* @typedef {Object} FileAndIgnored
21+
* @property {string} filePath The path to a target file.
22+
* @property {boolean} ignored If `true` then this file should be ignored and warned because it was directly specified.
23+
*/
24+
25+
/**
26+
* @typedef {Object} FileEntry
27+
* @property {string} filePath The path to a target file.
28+
* @property {ConfigArray} config The config entries of that file.
29+
* @property {NONE|IGNORED_SILENTLY|IGNORED} flag The flag.
30+
* - `NONE` means the file is a target file.
31+
* - `IGNORED_SILENTLY` means the file should be ignored silently.
32+
* - `IGNORED` means the file should be ignored and warned because it was directly specified.
33+
*/
34+
35+
/**
36+
* Get stats of a given path.
37+
* @param {string} filePath The path to target file.
38+
* @throws {Error} As may be thrown by `fs.statSync`.
39+
* @returns {fs.Stats|null} The stats.
40+
* @private
41+
*/
42+
function statSafeSync(filePath) {
43+
try {
44+
return fs.statSync(filePath);
45+
} catch (error) {
46+
/* c8 ignore next */
47+
if (error.code !== "ENOENT") {
48+
throw error;
49+
}
50+
return null;
51+
}
52+
}
53+
54+
/**
55+
* Get filenames in a given path to a directory.
56+
* @param {string} directoryPath The path to target directory.
57+
* @throws {Error} As may be thrown by `fs.readdirSync`.
58+
* @returns {import("fs").Dirent[]} The filenames.
59+
* @private
60+
*/
61+
function readdirSafeSync(directoryPath) {
62+
try {
63+
return fs.readdirSync(directoryPath, { withFileTypes: true });
64+
} catch (error) {
65+
/* c8 ignore next */
66+
if (error.code !== "ENOENT") {
67+
throw error;
68+
}
69+
return [];
70+
}
71+
}
72+
73+
/**
74+
* Create a `RegExp` object to detect extensions.
75+
* @param {string[] | null} extensions The extensions to create.
76+
* @returns {RegExp | null} The created `RegExp` object or null.
77+
*/
78+
function createExtensionRegExp(extensions) {
79+
if (extensions) {
80+
const normalizedExts = extensions.map((ext) =>
81+
escapeRegExp(ext.startsWith(".") ? ext.slice(1) : ext)
82+
);
83+
84+
return new RegExp(`.\\.(?:${normalizedExts.join("|")})$`, "u");
85+
}
86+
return null;
87+
}
88+
89+
/**
90+
* This class provides the functionality that enumerates every file which is
91+
* matched by given glob patterns and that configuration.
92+
*/
93+
export class FileEnumeratorIsh {
94+
/**
95+
* Initialize this enumerator.
96+
* @param {FileEnumeratorOptions} options The options.
97+
*/
98+
constructor({
99+
cwd = process.cwd(),
100+
extensions = null,
101+
isDirectoryIgnored,
102+
isFileIgnored,
103+
} = {}) {
104+
this.cwd = cwd;
105+
this.extensionRegExp = createExtensionRegExp(extensions);
106+
this.isDirectoryIgnored = isDirectoryIgnored;
107+
this.isFileIgnored = isFileIgnored;
108+
}
109+
110+
/**
111+
* Iterate files which are matched by given glob patterns.
112+
* @param {string|string[]} patternOrPatterns The glob patterns to iterate files.
113+
* @returns {IterableIterator<FileAndIgnored>} The found files.
114+
*/
115+
*iterateFiles(patternOrPatterns) {
116+
const patterns = Array.isArray(patternOrPatterns)
117+
? patternOrPatterns
118+
: [patternOrPatterns];
119+
120+
// The set of paths to remove duplicate.
121+
const set = new Set();
122+
123+
for (const pattern of patterns) {
124+
// Skip empty string.
125+
if (!pattern) {
126+
continue;
127+
}
128+
129+
// Iterate files of this pattern.
130+
for (const { filePath, flag } of this._iterateFiles(pattern)) {
131+
foundRegardlessOfIgnored = true;
132+
if (flag === IGNORED_SILENTLY) {
133+
continue;
134+
}
135+
found = true;
136+
137+
// Remove duplicate paths while yielding paths.
138+
if (!set.has(filePath)) {
139+
set.add(filePath);
140+
yield {
141+
filePath,
142+
ignored: flag === IGNORED,
143+
};
144+
}
145+
}
146+
}
147+
}
148+
149+
/**
150+
* Iterate files which are matched by a given glob pattern.
151+
* @param {string} pattern The glob pattern to iterate files.
152+
* @returns {IterableIterator<FileEntry>} The found files.
153+
*/
154+
_iterateFiles(pattern) {
155+
const { cwd } = this;
156+
const absolutePath = path.resolve(cwd, pattern);
157+
const isDot = dotfilesPattern.test(pattern);
158+
const stat = statSafeSync(absolutePath);
159+
160+
if (!stat) {
161+
return [];
162+
}
163+
164+
if (stat.isDirectory()) {
165+
return this._iterateFilesWithDirectory(absolutePath, isDot);
166+
}
167+
168+
if (stat.isFile()) {
169+
return this._iterateFilesWithFile(absolutePath);
170+
}
171+
}
172+
173+
/**
174+
* Iterate files in a given path.
175+
* @param {string} directoryPath The path to the target directory.
176+
* @param {boolean} dotfiles If `true` then it doesn't skip dot files by default.
177+
* @returns {IterableIterator<FileEntry>} The found files.
178+
* @private
179+
*/
180+
_iterateFilesWithDirectory(directoryPath, dotfiles) {
181+
return this._iterateFilesRecursive(directoryPath, { dotfiles });
182+
}
183+
184+
/**
185+
* Iterate files in a given path.
186+
* @param {string} directoryPath The path to the target directory.
187+
* @param {Object} options The options to iterate files.
188+
* @param {boolean} [options.dotfiles] If `true` then it doesn't skip dot files by default.
189+
* @param {boolean} [options.recursive] If `true` then it dives into sub directories.
190+
* @returns {IterableIterator<FileEntry>} The found files.
191+
* @private
192+
*/
193+
*_iterateFilesRecursive(directoryPath, options) {
194+
// Enumerate the files of this directory.
195+
for (const entry of readdirSafeSync(directoryPath)) {
196+
const filePath = path.join(directoryPath, entry.name);
197+
const fileInfo = entry.isSymbolicLink() ? statSafeSync(filePath) : entry;
198+
199+
if (!fileInfo) {
200+
continue;
201+
}
202+
203+
// Check if the file is matched.
204+
if (fileInfo.isFile()) {
205+
if (this.extensionRegExp.test(filePath)) {
206+
const ignored = this.isFileIgnored(filePath, options.dotfiles);
207+
const flag = ignored ? IGNORED_SILENTLY : NONE;
208+
209+
yield { filePath, flag };
210+
}
211+
212+
// Dive into the sub directory.
213+
} else if (fileInfo.isDirectory()) {
214+
const ignored = this.isDirectoryIgnored(
215+
filePath + path.sep,
216+
options.dotfiles
217+
);
218+
219+
if (!ignored) {
220+
yield* this._iterateFilesRecursive(filePath, options);
221+
}
222+
}
223+
}
224+
}
225+
}

src/rules/no-unused-modules.js

+15-48
Original file line numberDiff line numberDiff line change
@@ -15,53 +15,22 @@ import flatMap from 'array.prototype.flatmap';
1515

1616
import Exports, { recursivePatternCapture } from '../ExportMap';
1717
import docsUrl from '../docsUrl';
18+
import { FileEnumeratorIsh } from '../FileEnumeratorIsh';
1819

19-
let FileEnumerator;
20-
let listFilesToProcess;
21-
22-
try {
23-
({ FileEnumerator } = require('eslint/use-at-your-own-risk'));
24-
} catch (e) {
25-
try {
26-
// has been moved to eslint/lib/cli-engine/file-enumerator in version 6
27-
({ FileEnumerator } = require('eslint/lib/cli-engine/file-enumerator'));
28-
} catch (e) {
29-
try {
30-
// eslint/lib/util/glob-util has been moved to eslint/lib/util/glob-utils with version 5.3
31-
const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-utils');
32-
33-
// Prevent passing invalid options (extensions array) to old versions of the function.
34-
// https://github.com/eslint/eslint/blob/v5.16.0/lib/util/glob-utils.js#L178-L280
35-
// https://github.com/eslint/eslint/blob/v5.2.0/lib/util/glob-util.js#L174-L269
36-
listFilesToProcess = function (src, extensions) {
37-
return originalListFilesToProcess(src, {
38-
extensions,
39-
});
40-
};
41-
} catch (e) {
42-
const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-util');
43-
44-
listFilesToProcess = function (src, extensions) {
45-
const patterns = src.concat(flatMap(src, (pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`)));
46-
47-
return originalListFilesToProcess(patterns);
48-
};
49-
}
50-
}
51-
}
20+
const listFilesToProcess = function (context, src) {
21+
const extensions = Array.from(getFileExtensions(context.settings));
5222

53-
if (FileEnumerator) {
54-
listFilesToProcess = function (src, extensions) {
55-
const e = new FileEnumerator({
56-
extensions,
57-
});
23+
const e = new FileEnumeratorIsh({
24+
cwd: context.cwd,
25+
extensions,
26+
...context.session,
27+
});
5828

59-
return Array.from(e.iterateFiles(src), ({ filePath, ignored }) => ({
60-
ignored,
61-
filename: filePath,
62-
}));
63-
};
64-
}
29+
return Array.from(e.iterateFiles(src), ({ filePath, ignored }) => ({
30+
ignored,
31+
filename: filePath,
32+
}));
33+
};
6534

6635
const EXPORT_DEFAULT_DECLARATION = 'ExportDefaultDeclaration';
6736
const EXPORT_NAMED_DECLARATION = 'ExportNamedDeclaration';
@@ -171,12 +140,10 @@ const isNodeModule = (path) => (/\/(node_modules)\//).test(path);
171140
* return all files matching src pattern, which are not matching the ignoreExports pattern
172141
*/
173142
const resolveFiles = (src, ignoreExports, context) => {
174-
const extensions = Array.from(getFileExtensions(context.settings));
175-
176-
const srcFileList = listFilesToProcess(src, extensions);
143+
const srcFileList = listFilesToProcess(context, src);
177144

178145
// prepare list of ignored files
179-
const ignoredFilesList = listFilesToProcess(ignoreExports, extensions);
146+
const ignoredFilesList = listFilesToProcess(context, ignoreExports);
180147
ignoredFilesList.forEach(({ filename }) => ignoredFiles.add(filename));
181148

182149
// prepare list of source files, don't consider files from node_modules

0 commit comments

Comments
 (0)