Skip to content

Commit 9d677db

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

File tree

2 files changed

+236
-48
lines changed

2 files changed

+236
-48
lines changed

src/FileEnumeratorIsh.js

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

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)