Skip to content

Commit

Permalink
[PoC] Created FileEnumeratorIsh to demonstrate flat config support
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg committed Feb 24, 2024
1 parent e55d05a commit 9d677db
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 48 deletions.
221 changes: 221 additions & 0 deletions src/FileEnumeratorIsh.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
const fs = require("fs");
const path = require("path");
const escapeRegExp = require("escape-string-regexp");

const dotfilesPattern = /(?:(?:^\.)|(?:[/\\]\.))[^/\\.].*/u;
const NONE = 0;
const IGNORED_SILENTLY = 1;

/**
* @typedef {Object} FileEnumeratorOptions
* @property {CascadingConfigArrayFactory} [configArrayFactory] The factory for config arrays.
* @property {string} [cwd] The base directory to start lookup.
* @property {string[]} [extensions] The extensions to match files for directory patterns.
* @property {(directoryPath: string) => boolean} [isDirectoryIgnored] Returns whether a directory is ignored.
* @property {(filePath: string) => boolean} [isFileIgnored] Returns whether a file is ignored.
*/

/**
* @typedef {Object} FileAndIgnored
* @property {string} filePath The path to a target file.
* @property {boolean} ignored If `true` then this file should be ignored and warned because it was directly specified.
*/

/**
* @typedef {Object} FileEntry
* @property {string} filePath The path to a target file.
* @property {ConfigArray} config The config entries of that file.
* @property {NONE|IGNORED_SILENTLY} flag The flag.
* - `NONE` means the file is a target file.
* - `IGNORED_SILENTLY` means the file should be ignored silently.
*/

/**
* Get stats of a given path.
* @param {string} filePath The path to target file.
* @throws {Error} As may be thrown by `fs.statSync`.
* @returns {fs.Stats|null} The stats.
* @private
*/
function statSafeSync(filePath) {
try {
return fs.statSync(filePath);
} catch (error) {
/* c8 ignore next */
if (error.code !== "ENOENT") {
throw error;
}
return null;
}
}

/**
* Get filenames in a given path to a directory.
* @param {string} directoryPath The path to target directory.
* @throws {Error} As may be thrown by `fs.readdirSync`.
* @returns {import("fs").Dirent[]} The filenames.
* @private
*/
function readdirSafeSync(directoryPath) {
try {
return fs.readdirSync(directoryPath, { withFileTypes: true });
} catch (error) {
/* c8 ignore next */
if (error.code !== "ENOENT") {
throw error;
}
return [];
}
}

/**
* Create a `RegExp` object to detect extensions.
* @param {string[] | null} extensions The extensions to create.
* @returns {RegExp | null} The created `RegExp` object or null.
*/
function createExtensionRegExp(extensions) {
if (extensions) {
const normalizedExts = extensions.map((ext) =>
escapeRegExp(ext.startsWith(".") ? ext.slice(1) : ext)
);

return new RegExp(`.\\.(?:${normalizedExts.join("|")})$`, "u");
}
return null;
}

/**
* This class provides the functionality that enumerates every file which is
* matched by given glob patterns and that configuration.
*/
export class FileEnumeratorIsh {
/**
* Initialize this enumerator.
* @param {FileEnumeratorOptions} options The options.
*/
constructor({
cwd = process.cwd(),
extensions = null,
isDirectoryIgnored,
isFileIgnored,
} = {}) {
this.cwd = cwd;
this.extensionRegExp = createExtensionRegExp(extensions);
this.isDirectoryIgnored = isDirectoryIgnored;
this.isFileIgnored = isFileIgnored;
}

/**
* Iterate files which are matched by given glob patterns.
* @param {string|string[]} patternOrPatterns The glob patterns to iterate files.
* @returns {IterableIterator<FileAndIgnored>} The found files.
*/
*iterateFiles(patternOrPatterns) {
const patterns = Array.isArray(patternOrPatterns)
? patternOrPatterns
: [patternOrPatterns];

// The set of paths to remove duplicate.
const set = new Set();

for (const pattern of patterns) {
// Skip empty string.
if (!pattern) {
continue;
}

// Iterate files of this pattern.
for (const { filePath, flag } of this._iterateFiles(pattern)) {
if (flag === IGNORED_SILENTLY) {
continue;
}

// Remove duplicate paths while yielding paths.
if (!set.has(filePath)) {
set.add(filePath);
yield {
filePath,
ignored: false,
};
}
}
}
}

/**
* Iterate files which are matched by a given glob pattern.
* @param {string} pattern The glob pattern to iterate files.
* @returns {IterableIterator<FileEntry>} The found files.
*/
_iterateFiles(pattern) {
const { cwd } = this;
const absolutePath = path.resolve(cwd, pattern);
const isDot = dotfilesPattern.test(pattern);
const stat = statSafeSync(absolutePath);

if (!stat) {
return [];
}

if (stat.isDirectory()) {
return this._iterateFilesWithDirectory(absolutePath, isDot);
}

if (stat.isFile()) {
return this._iterateFilesWithFile(absolutePath);
}
}

/**
* Iterate files in a given path.
* @param {string} directoryPath The path to the target directory.
* @param {boolean} dotfiles If `true` then it doesn't skip dot files by default.
* @returns {IterableIterator<FileEntry>} The found files.
* @private
*/
_iterateFilesWithDirectory(directoryPath, dotfiles) {
return this._iterateFilesRecursive(directoryPath, { dotfiles });
}

/**
* Iterate files in a given path.
* @param {string} directoryPath The path to the target directory.
* @param {Object} options The options to iterate files.
* @param {boolean} [options.dotfiles] If `true` then it doesn't skip dot files by default.
* @param {boolean} [options.recursive] If `true` then it dives into sub directories.
* @returns {IterableIterator<FileEntry>} The found files.
* @private
*/
*_iterateFilesRecursive(directoryPath, options) {
// Enumerate the files of this directory.
for (const entry of readdirSafeSync(directoryPath)) {
const filePath = path.join(directoryPath, entry.name);
const fileInfo = entry.isSymbolicLink() ? statSafeSync(filePath) : entry;

if (!fileInfo) {
continue;
}

// Check if the file is matched.
if (fileInfo.isFile()) {
if (this.extensionRegExp.test(filePath)) {
const ignored = this.isFileIgnored(filePath, options.dotfiles);
const flag = ignored ? IGNORED_SILENTLY : NONE;

yield { filePath, flag };
}

// Dive into the sub directory.
} else if (fileInfo.isDirectory()) {
const ignored = this.isDirectoryIgnored(
filePath + path.sep,
options.dotfiles
);

if (!ignored) {
yield* this._iterateFilesRecursive(filePath, options);
}
}
}
}
}
63 changes: 15 additions & 48 deletions src/rules/no-unused-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,53 +15,22 @@ import flatMap from 'array.prototype.flatmap';

import Exports, { recursivePatternCapture } from '../ExportMap';
import docsUrl from '../docsUrl';
import { FileEnumeratorIsh } from '../FileEnumeratorIsh';

let FileEnumerator;
let listFilesToProcess;

try {
({ FileEnumerator } = require('eslint/use-at-your-own-risk'));
} catch (e) {
try {
// has been moved to eslint/lib/cli-engine/file-enumerator in version 6
({ FileEnumerator } = require('eslint/lib/cli-engine/file-enumerator'));
} catch (e) {
try {
// eslint/lib/util/glob-util has been moved to eslint/lib/util/glob-utils with version 5.3
const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-utils');

// Prevent passing invalid options (extensions array) to old versions of the function.
// https://github.com/eslint/eslint/blob/v5.16.0/lib/util/glob-utils.js#L178-L280
// https://github.com/eslint/eslint/blob/v5.2.0/lib/util/glob-util.js#L174-L269
listFilesToProcess = function (src, extensions) {
return originalListFilesToProcess(src, {
extensions,
});
};
} catch (e) {
const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-util');

listFilesToProcess = function (src, extensions) {
const patterns = src.concat(flatMap(src, (pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`)));

return originalListFilesToProcess(patterns);
};
}
}
}
const listFilesToProcess = function (context, src) {
const extensions = Array.from(getFileExtensions(context.settings));

if (FileEnumerator) {
listFilesToProcess = function (src, extensions) {
const e = new FileEnumerator({
extensions,
});
const e = new FileEnumeratorIsh({
cwd: context.cwd,
extensions,
...context.session,
});

return Array.from(e.iterateFiles(src), ({ filePath, ignored }) => ({
ignored,
filename: filePath,
}));
};
}
return Array.from(e.iterateFiles(src), ({ filePath, ignored }) => ({
ignored,
filename: filePath,
}));
};

const EXPORT_DEFAULT_DECLARATION = 'ExportDefaultDeclaration';
const EXPORT_NAMED_DECLARATION = 'ExportNamedDeclaration';
Expand Down Expand Up @@ -171,12 +140,10 @@ const isNodeModule = (path) => (/\/(node_modules)\//).test(path);
* return all files matching src pattern, which are not matching the ignoreExports pattern
*/
const resolveFiles = (src, ignoreExports, context) => {
const extensions = Array.from(getFileExtensions(context.settings));

const srcFileList = listFilesToProcess(src, extensions);
const srcFileList = listFilesToProcess(context, src);

// prepare list of ignored files
const ignoredFilesList = listFilesToProcess(ignoreExports, extensions);
const ignoredFilesList = listFilesToProcess(context, ignoreExports);
ignoredFilesList.forEach(({ filename }) => ignoredFiles.add(filename));

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

0 comments on commit 9d677db

Please sign in to comment.