Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PoC] Created FileEnumeratorIsh to demonstrate flat config support #2967

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
221 changes: 221 additions & 0 deletions src/FileEnumeratorIsh.js
@@ -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
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