1466 lines
44 KiB
JavaScript
1466 lines
44 KiB
JavaScript
/**
|
|
* @fileoverview Helper functions for ESLint class
|
|
* @author Nicholas C. Zakas
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Requirements
|
|
//-----------------------------------------------------------------------------
|
|
|
|
const path = require("node:path");
|
|
const fs = require("node:fs");
|
|
const { isMainThread, threadId } = require("node:worker_threads");
|
|
const fsp = fs.promises;
|
|
const isGlob = require("is-glob");
|
|
const hash = require("../cli-engine/hash");
|
|
const minimatch = require("minimatch");
|
|
const globParent = require("glob-parent");
|
|
const { Linter } = require("../linter");
|
|
const { getShorthandName } = require("../shared/naming");
|
|
const LintResultCache = require("../cli-engine/lint-result-cache");
|
|
const { ConfigLoader, LegacyConfigLoader } = require("../config/config-loader");
|
|
const createDebug = require("debug");
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Fixup references
|
|
//-----------------------------------------------------------------------------
|
|
|
|
const Minimatch = minimatch.Minimatch;
|
|
const MINIMATCH_OPTIONS = { dot: true };
|
|
const hrtimeBigint = process.hrtime.bigint;
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Types
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @import { ESLintOptions } from "./eslint.js";
|
|
* @import { Config as CalculatedConfig } from "../config/config.js";
|
|
* @import { FlatConfigArray } from "../config/flat-config-array.js";
|
|
* @import { WarningService } from "../services/warning-service.js";
|
|
* @import { Retrier } from "@humanwhocodes/retry";
|
|
*/
|
|
|
|
/** @typedef {import("../types").Linter.Config} Config */
|
|
/** @typedef {import("../types").Linter.LintMessage} LintMessage */
|
|
/** @typedef {import("../types").ESLint.LintResult} LintResult */
|
|
/** @typedef {import("../types").ESLint.Plugin} Plugin */
|
|
|
|
/**
|
|
* @typedef {Object} GlobSearch
|
|
* @property {Array<string>} patterns The normalized patterns to use for a search.
|
|
* @property {Array<string>} rawPatterns The patterns as entered by the user
|
|
* before doing any normalization.
|
|
*/
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Debug Helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Add %t formatter to print bigint nanosecond times in milliseconds.
|
|
createDebug.formatters.t = timeDiff =>
|
|
`${(timeDiff + 500_000n) / 1_000_000n} ms`;
|
|
|
|
const debug = createDebug(
|
|
`eslint:eslint-helpers${isMainThread ? "" : `:thread-${threadId}`}`,
|
|
);
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Errors
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* The error type when no files match a glob.
|
|
*/
|
|
class NoFilesFoundError extends Error {
|
|
/**
|
|
* @param {string} pattern The glob pattern which was not found.
|
|
* @param {boolean} globEnabled If `false` then the pattern was a glob pattern, but glob was disabled.
|
|
*/
|
|
constructor(pattern, globEnabled) {
|
|
super(
|
|
`No files matching '${pattern}' were found${!globEnabled ? " (glob was disabled)" : ""}.`,
|
|
);
|
|
this.messageTemplate = "file-not-found";
|
|
this.messageData = { pattern, globDisabled: !globEnabled };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The error type when a search fails to match multiple patterns.
|
|
*/
|
|
class UnmatchedSearchPatternsError extends Error {
|
|
/**
|
|
* @param {Object} options The options for the error.
|
|
* @param {string} options.basePath The directory that was searched.
|
|
* @param {Array<string>} options.unmatchedPatterns The glob patterns
|
|
* which were not found.
|
|
* @param {Array<string>} options.patterns The glob patterns that were
|
|
* searched.
|
|
* @param {Array<string>} options.rawPatterns The raw glob patterns that
|
|
* were searched.
|
|
*/
|
|
constructor({ basePath, unmatchedPatterns, patterns, rawPatterns }) {
|
|
super(
|
|
`No files matching '${rawPatterns}' in '${basePath}' were found.`,
|
|
);
|
|
this.basePath = basePath;
|
|
this.unmatchedPatterns = unmatchedPatterns;
|
|
this.patterns = patterns;
|
|
this.rawPatterns = rawPatterns;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The error type when there are files matched by a glob, but all of them have been ignored.
|
|
*/
|
|
class AllFilesIgnoredError extends Error {
|
|
/**
|
|
* @param {string} pattern The glob pattern which was not found.
|
|
*/
|
|
constructor(pattern) {
|
|
super(`All files matched by '${pattern}' are ignored.`);
|
|
this.messageTemplate = "all-matched-files-ignored";
|
|
this.messageData = { pattern };
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// General Helpers
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Check if a given value is a non-empty string or not.
|
|
* @param {any} value The value to check.
|
|
* @returns {boolean} `true` if `value` is a non-empty string.
|
|
*/
|
|
function isNonEmptyString(value) {
|
|
return typeof value === "string" && value.trim() !== "";
|
|
}
|
|
|
|
/**
|
|
* Check if a given value is an array of non-empty strings or not.
|
|
* @param {any} value The value to check.
|
|
* @returns {boolean} `true` if `value` is an array of non-empty strings.
|
|
*/
|
|
function isArrayOfNonEmptyString(value) {
|
|
return (
|
|
Array.isArray(value) && !!value.length && value.every(isNonEmptyString)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if a given value is an empty array or an array of non-empty strings.
|
|
* @param {any} value The value to check.
|
|
* @returns {boolean} `true` if `value` is an empty array or an array of non-empty
|
|
* strings.
|
|
*/
|
|
function isEmptyArrayOrArrayOfNonEmptyString(value) {
|
|
return Array.isArray(value) && value.every(isNonEmptyString);
|
|
}
|
|
|
|
/**
|
|
* Check if a given value is a positive integer.
|
|
* @param {unknown} value The value to check.
|
|
* @returns {boolean} `true` if `value` is a positive integer.
|
|
*/
|
|
function isPositiveInteger(value) {
|
|
return Number.isInteger(value) && value > 0;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// File-related Helpers
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Normalizes slashes in a file pattern to posix-style.
|
|
* @param {string} pattern The pattern to replace slashes in.
|
|
* @returns {string} The pattern with slashes normalized.
|
|
*/
|
|
function normalizeToPosix(pattern) {
|
|
return pattern.replace(/\\/gu, "/");
|
|
}
|
|
|
|
/**
|
|
* Check if a string is a glob pattern or not.
|
|
* @param {string} pattern A glob pattern.
|
|
* @returns {boolean} `true` if the string is a glob pattern.
|
|
*/
|
|
function isGlobPattern(pattern) {
|
|
return isGlob(path.sep === "\\" ? normalizeToPosix(pattern) : pattern);
|
|
}
|
|
|
|
/**
|
|
* Determines if a given glob pattern will return any results.
|
|
* Used primarily to help with useful error messages.
|
|
* @param {Object} options The options for the function.
|
|
* @param {string} options.basePath The directory to search.
|
|
* @param {string} options.pattern An absolute path glob pattern to match.
|
|
* @returns {Promise<boolean>} True if there is a glob match, false if not.
|
|
*/
|
|
async function globMatch({ basePath, pattern }) {
|
|
let found = false;
|
|
const { hfs } = await import("@humanfs/node");
|
|
const patternToUse = normalizeToPosix(path.relative(basePath, pattern));
|
|
|
|
const matcher = new Minimatch(patternToUse, MINIMATCH_OPTIONS);
|
|
|
|
const walkSettings = {
|
|
directoryFilter(entry) {
|
|
return !found && matcher.match(entry.path, true);
|
|
},
|
|
|
|
entryFilter(entry) {
|
|
if (found || entry.isDirectory) {
|
|
return false;
|
|
}
|
|
|
|
if (matcher.match(entry.path)) {
|
|
found = true;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
};
|
|
|
|
if (await hfs.isDirectory(basePath)) {
|
|
return hfs
|
|
.walk(basePath, walkSettings)
|
|
.next()
|
|
.then(() => found);
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
/**
|
|
* Searches a directory looking for matching glob patterns. This uses
|
|
* the config array's logic to determine if a directory or file should
|
|
* be ignored, so it is consistent with how ignoring works throughout
|
|
* ESLint.
|
|
* @param {Object} options The options for this function.
|
|
* @param {string} options.basePath The directory to search.
|
|
* @param {Array<string>} options.patterns An array of absolute path glob patterns
|
|
* to match.
|
|
* @param {Array<string>} options.rawPatterns An array of glob patterns
|
|
* as the user inputted them. Used for errors.
|
|
* @param {ConfigLoader|LegacyConfigLoader} options.configLoader The config array to use for
|
|
* determining what to ignore.
|
|
* @param {boolean} options.errorOnUnmatchedPattern Determines if an error
|
|
* should be thrown when a pattern is unmatched.
|
|
* @returns {Promise<Array<string>>} An array of matching file paths
|
|
* or an empty array if there are no matches.
|
|
* @throws {UnmatchedSearchPatternsError} If there is a pattern that doesn't
|
|
* match any files.
|
|
*/
|
|
async function globSearch({
|
|
basePath,
|
|
patterns,
|
|
rawPatterns,
|
|
configLoader,
|
|
errorOnUnmatchedPattern,
|
|
}) {
|
|
if (patterns.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
/*
|
|
* In this section we are converting the patterns into Minimatch
|
|
* instances for performance reasons. Because we are doing the same
|
|
* matches repeatedly, it's best to compile those patterns once and
|
|
* reuse them multiple times.
|
|
*
|
|
* To do that, we convert any patterns with an absolute path into a
|
|
* relative path and normalize it to Posix-style slashes. We also keep
|
|
* track of the relative patterns to map them back to the original
|
|
* patterns, which we need in order to throw an error if there are any
|
|
* unmatched patterns.
|
|
*/
|
|
const relativeToPatterns = new Map();
|
|
const matchers = patterns.map((pattern, i) => {
|
|
const patternToUse = normalizeToPosix(path.relative(basePath, pattern));
|
|
|
|
relativeToPatterns.set(patternToUse, patterns[i]);
|
|
|
|
return new Minimatch(patternToUse, MINIMATCH_OPTIONS);
|
|
});
|
|
|
|
/*
|
|
* We track unmatched patterns because we may want to throw an error when
|
|
* they occur. To start, this set is initialized with all of the patterns.
|
|
* Every time a match occurs, the pattern is removed from the set, making
|
|
* it easy to tell if we have any unmatched patterns left at the end of
|
|
* search.
|
|
*/
|
|
const unmatchedPatterns = new Set([...relativeToPatterns.keys()]);
|
|
const { hfs } = await import("@humanfs/node");
|
|
|
|
const walk = hfs.walk(basePath, {
|
|
async directoryFilter(entry) {
|
|
if (!matchers.some(matcher => matcher.match(entry.path, true))) {
|
|
return false;
|
|
}
|
|
|
|
const absolutePath = path.resolve(basePath, entry.path);
|
|
const configs =
|
|
await configLoader.loadConfigArrayForDirectory(absolutePath);
|
|
|
|
return !configs.isDirectoryIgnored(absolutePath);
|
|
},
|
|
async entryFilter(entry) {
|
|
const absolutePath = path.resolve(basePath, entry.path);
|
|
|
|
// entries may be directories or files so filter out directories
|
|
if (entry.isDirectory) {
|
|
return false;
|
|
}
|
|
|
|
const configs =
|
|
await configLoader.loadConfigArrayForFile(absolutePath);
|
|
const config = configs.getConfig(absolutePath);
|
|
|
|
/*
|
|
* Optimization: We need to track when patterns are left unmatched
|
|
* and so we use `unmatchedPatterns` to do that. There is a bit of
|
|
* complexity here because the same file can be matched by more than
|
|
* one pattern. So, when we start, we actually need to test every
|
|
* pattern against every file. Once we know there are no remaining
|
|
* unmatched patterns, then we can switch to just looking for the
|
|
* first matching pattern for improved speed.
|
|
*/
|
|
const matchesPattern =
|
|
unmatchedPatterns.size > 0
|
|
? matchers.reduce((previousValue, matcher) => {
|
|
const pathMatches = matcher.match(entry.path);
|
|
|
|
/*
|
|
* We updated the unmatched patterns set only if the path
|
|
* matches and the file has a config. If the file has no
|
|
* config, that means there wasn't a match for the
|
|
* pattern so it should not be removed.
|
|
*
|
|
* Performance note: `getConfig()` aggressively caches
|
|
* results so there is no performance penalty for calling
|
|
* it multiple times with the same argument.
|
|
*/
|
|
if (pathMatches && config) {
|
|
unmatchedPatterns.delete(matcher.pattern);
|
|
}
|
|
|
|
return pathMatches || previousValue;
|
|
}, false)
|
|
: matchers.some(matcher => matcher.match(entry.path));
|
|
|
|
return matchesPattern && config !== void 0;
|
|
},
|
|
});
|
|
|
|
const filePaths = [];
|
|
|
|
if (await hfs.isDirectory(basePath)) {
|
|
for await (const entry of walk) {
|
|
filePaths.push(path.resolve(basePath, entry.path));
|
|
}
|
|
}
|
|
|
|
// now check to see if we have any unmatched patterns
|
|
if (errorOnUnmatchedPattern && unmatchedPatterns.size > 0) {
|
|
throw new UnmatchedSearchPatternsError({
|
|
basePath,
|
|
unmatchedPatterns: [...unmatchedPatterns].map(pattern =>
|
|
relativeToPatterns.get(pattern),
|
|
),
|
|
patterns,
|
|
rawPatterns,
|
|
});
|
|
}
|
|
|
|
return filePaths;
|
|
}
|
|
|
|
/**
|
|
* Throws an error for unmatched patterns. The error will only contain information about the first one.
|
|
* Checks to see if there are any ignored results for a given search.
|
|
* @param {Object} options The options for this function.
|
|
* @param {string} options.basePath The directory to search.
|
|
* @param {Array<string>} options.patterns An array of glob patterns
|
|
* that were used in the original search.
|
|
* @param {Array<string>} options.rawPatterns An array of glob patterns
|
|
* as the user inputted them. Used for errors.
|
|
* @param {Array<string>} options.unmatchedPatterns A non-empty array of absolute path glob patterns
|
|
* that were unmatched in the original search.
|
|
* @returns {Promise<never>} Always throws an error.
|
|
* @throws {NoFilesFoundError} If the first unmatched pattern
|
|
* doesn't match any files even when there are no ignores.
|
|
* @throws {AllFilesIgnoredError} If the first unmatched pattern
|
|
* matches some files when there are no ignores.
|
|
*/
|
|
async function throwErrorForUnmatchedPatterns({
|
|
basePath,
|
|
patterns,
|
|
rawPatterns,
|
|
unmatchedPatterns,
|
|
}) {
|
|
const pattern = unmatchedPatterns[0];
|
|
const rawPattern = rawPatterns[patterns.indexOf(pattern)];
|
|
|
|
const patternHasMatch = await globMatch({
|
|
basePath,
|
|
pattern,
|
|
});
|
|
|
|
if (patternHasMatch) {
|
|
throw new AllFilesIgnoredError(rawPattern);
|
|
}
|
|
|
|
// if we get here there are truly no matches
|
|
throw new NoFilesFoundError(rawPattern, true);
|
|
}
|
|
|
|
/**
|
|
* Performs multiple glob searches in parallel.
|
|
* @param {Object} options The options for this function.
|
|
* @param {Map<string,GlobSearch>} options.searches
|
|
* A map of absolute path glob patterns to match.
|
|
* @param {ConfigLoader|LegacyConfigLoader} options.configLoader The config loader to use for
|
|
* determining what to ignore.
|
|
* @param {boolean} options.errorOnUnmatchedPattern Determines if an
|
|
* unmatched glob pattern should throw an error.
|
|
* @returns {Promise<Array<string>>} An array of matching file paths
|
|
* or an empty array if there are no matches.
|
|
*/
|
|
async function globMultiSearch({
|
|
searches,
|
|
configLoader,
|
|
errorOnUnmatchedPattern,
|
|
}) {
|
|
/*
|
|
* For convenience, we normalized the search map into an array of objects.
|
|
* Next, we filter out all searches that have no patterns. This happens
|
|
* primarily for the cwd, which is prepopulated in the searches map as an
|
|
* optimization. However, if it has no patterns, it means all patterns
|
|
* occur outside of the cwd and we can safely filter out that search.
|
|
*/
|
|
const normalizedSearches = [...searches]
|
|
.map(([basePath, { patterns, rawPatterns }]) => ({
|
|
basePath,
|
|
patterns,
|
|
rawPatterns,
|
|
}))
|
|
.filter(({ patterns }) => patterns.length > 0);
|
|
|
|
const results = await Promise.allSettled(
|
|
normalizedSearches.map(({ basePath, patterns, rawPatterns }) =>
|
|
globSearch({
|
|
basePath,
|
|
patterns,
|
|
rawPatterns,
|
|
configLoader,
|
|
errorOnUnmatchedPattern,
|
|
}),
|
|
),
|
|
);
|
|
|
|
/*
|
|
* The first loop handles errors from the glob searches. Since we can't
|
|
* use `await` inside `flatMap`, we process errors separately in this loop.
|
|
* This results in two iterations over `results`, but since the length is
|
|
* less than or equal to the number of globs and directories passed on the
|
|
* command line, the performance impact should be minimal.
|
|
*/
|
|
for (let i = 0; i < results.length; i++) {
|
|
const result = results[i];
|
|
const currentSearch = normalizedSearches[i];
|
|
|
|
if (result.status === "fulfilled") {
|
|
continue;
|
|
}
|
|
|
|
// if we make it here then there was an error
|
|
const error = result.reason;
|
|
|
|
// unexpected errors should be re-thrown
|
|
if (!error.basePath) {
|
|
throw error;
|
|
}
|
|
|
|
if (errorOnUnmatchedPattern) {
|
|
await throwErrorForUnmatchedPatterns({
|
|
...currentSearch,
|
|
unmatchedPatterns: error.unmatchedPatterns,
|
|
});
|
|
}
|
|
}
|
|
|
|
// second loop for `fulfilled` results
|
|
return results.flatMap(result => result.value);
|
|
}
|
|
|
|
/**
|
|
* Finds all files matching the options specified.
|
|
* @param {Object} args The arguments objects.
|
|
* @param {Array<string>} args.patterns An array of glob patterns.
|
|
* @param {boolean} args.globInputPaths true to interpret glob patterns,
|
|
* false to not interpret glob patterns.
|
|
* @param {string} args.cwd The current working directory to find from.
|
|
* @param {ConfigLoader|LegacyConfigLoader} args.configLoader The config loader for the current run.
|
|
* @param {boolean} args.errorOnUnmatchedPattern Determines if an unmatched pattern
|
|
* should throw an error.
|
|
* @returns {Promise<Array<string>>} The fully resolved file paths.
|
|
* @throws {AllFilesIgnoredError} If there are no results due to an ignore pattern.
|
|
* @throws {NoFilesFoundError} If no files matched the given patterns.
|
|
*/
|
|
async function findFiles({
|
|
patterns,
|
|
globInputPaths,
|
|
cwd,
|
|
configLoader,
|
|
errorOnUnmatchedPattern,
|
|
}) {
|
|
const results = [];
|
|
const missingPatterns = [];
|
|
let globbyPatterns = [];
|
|
let rawPatterns = [];
|
|
const searches = new Map([
|
|
[cwd, { patterns: globbyPatterns, rawPatterns: [] }],
|
|
]);
|
|
|
|
/*
|
|
* This part is a bit involved because we need to account for
|
|
* the different ways that the patterns can match directories.
|
|
* For each different way, we need to decide if we should look
|
|
* for a config file or just use the default config. (Directories
|
|
* without a config file always use the default config.)
|
|
*
|
|
* Here are the cases:
|
|
*
|
|
* 1. A directory is passed directly (e.g., "subdir"). In this case, we
|
|
* can assume that the user intends to lint this directory and we should
|
|
* not look for a config file in the parent directory, because the only
|
|
* reason to do that would be to ignore this directory (which we already
|
|
* know we don't want to do). Instead, we use the default config until we
|
|
* get to the directory that was passed, at which point we start looking
|
|
* for config files again.
|
|
*
|
|
* 2. A dot (".") or star ("*"). In this case, we want to read
|
|
* the config file in the current directory because the user is
|
|
* explicitly asking to lint the current directory. Note that "."
|
|
* will traverse into subdirectories while "*" will not.
|
|
*
|
|
* 3. A directory is passed in the form of "subdir/subsubdir".
|
|
* In this case, we don't want to look for a config file in the
|
|
* parent directory ("subdir"). We can skip looking for a config
|
|
* file until `entry.depth` is greater than 1 because there's no
|
|
* way that the pattern can match `entry.path` yet.
|
|
*
|
|
* 4. A directory glob pattern is passed (e.g., "subd*"). We want
|
|
* this case to act like case 2 because it's unclear whether or not
|
|
* any particular directory is meant to be traversed.
|
|
*
|
|
* 5. A recursive glob pattern is passed (e.g., "**"). We want this
|
|
* case to act like case 2.
|
|
*/
|
|
|
|
// check to see if we have explicit files and directories
|
|
const filePaths = patterns.map(filePath => path.resolve(cwd, filePath));
|
|
const stats = await Promise.all(
|
|
filePaths.map(filePath => fsp.stat(filePath).catch(() => {})),
|
|
);
|
|
|
|
const promises = [];
|
|
stats.forEach((stat, index) => {
|
|
const filePath = filePaths[index];
|
|
const pattern = normalizeToPosix(patterns[index]);
|
|
|
|
if (stat) {
|
|
// files are added directly to the list
|
|
if (stat.isFile()) {
|
|
results.push(filePath);
|
|
promises.push(configLoader.loadConfigArrayForFile(filePath));
|
|
}
|
|
|
|
// directories need extensions attached
|
|
if (stat.isDirectory()) {
|
|
if (!searches.has(filePath)) {
|
|
searches.set(filePath, { patterns: [], rawPatterns: [] });
|
|
}
|
|
({ patterns: globbyPatterns, rawPatterns } =
|
|
searches.get(filePath));
|
|
|
|
globbyPatterns.push(`${normalizeToPosix(filePath)}/**`);
|
|
rawPatterns.push(pattern);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// save patterns for later use based on whether globs are enabled
|
|
if (globInputPaths && isGlobPattern(pattern)) {
|
|
/*
|
|
* We are grouping patterns by their glob parent. This is done to
|
|
* make it easier to determine when a config file should be loaded.
|
|
*/
|
|
|
|
const basePath = path.resolve(cwd, globParent(pattern));
|
|
|
|
if (!searches.has(basePath)) {
|
|
searches.set(basePath, { patterns: [], rawPatterns: [] });
|
|
}
|
|
({ patterns: globbyPatterns, rawPatterns } =
|
|
searches.get(basePath));
|
|
|
|
globbyPatterns.push(filePath);
|
|
rawPatterns.push(pattern);
|
|
} else {
|
|
missingPatterns.push(pattern);
|
|
}
|
|
});
|
|
|
|
// there were patterns that didn't match anything, tell the user
|
|
if (errorOnUnmatchedPattern && missingPatterns.length) {
|
|
throw new NoFilesFoundError(missingPatterns[0], globInputPaths);
|
|
}
|
|
|
|
// now we are safe to do the search
|
|
promises.push(
|
|
globMultiSearch({
|
|
searches,
|
|
configLoader,
|
|
errorOnUnmatchedPattern,
|
|
}),
|
|
);
|
|
const globbyResults = (await Promise.all(promises)).at(-1);
|
|
|
|
return [...new Set([...results, ...globbyResults])];
|
|
}
|
|
|
|
/**
|
|
* Return the absolute path of a file named `"__placeholder__.js"` in a given directory.
|
|
* This is used as a replacement for a missing file path.
|
|
* @param {string} cwd An absolute directory path.
|
|
* @returns {string} The absolute path of a file named `"__placeholder__.js"` in the given directory.
|
|
*/
|
|
function getPlaceholderPath(cwd) {
|
|
return path.join(cwd, "__placeholder__.js");
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Results-related Helpers
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Checks if the given message is an error message.
|
|
* @param {LintMessage} message The message to check.
|
|
* @returns {boolean} Whether or not the message is an error message.
|
|
* @private
|
|
*/
|
|
function isErrorMessage(message) {
|
|
return message.severity === 2;
|
|
}
|
|
|
|
/**
|
|
* Returns result with warning by ignore settings
|
|
* @param {string} filePath Absolute file path of checked code
|
|
* @param {string} baseDir Absolute path of base directory
|
|
* @param {"ignored"|"external"|"unconfigured"} configStatus A status that determines why the file is ignored
|
|
* @returns {LintResult} Result with single warning
|
|
* @private
|
|
*/
|
|
function createIgnoreResult(filePath, baseDir, configStatus) {
|
|
let message;
|
|
|
|
switch (configStatus) {
|
|
case "external":
|
|
message = "File ignored because outside of base path.";
|
|
break;
|
|
case "unconfigured":
|
|
message =
|
|
"File ignored because no matching configuration was supplied.";
|
|
break;
|
|
default:
|
|
{
|
|
const isInNodeModules =
|
|
baseDir &&
|
|
path
|
|
.dirname(path.relative(baseDir, filePath))
|
|
.split(path.sep)
|
|
.includes("node_modules");
|
|
|
|
if (isInNodeModules) {
|
|
message =
|
|
'File ignored by default because it is located under the node_modules directory. Use ignore pattern "!**/node_modules/" to disable file ignore settings or use "--no-warn-ignored" to suppress this warning.';
|
|
} else {
|
|
message =
|
|
'File ignored because of a matching ignore pattern. Use "--no-ignore" to disable file ignore settings or use "--no-warn-ignored" to suppress this warning.';
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return {
|
|
filePath,
|
|
messages: [
|
|
{
|
|
ruleId: null,
|
|
fatal: false,
|
|
severity: 1,
|
|
message,
|
|
nodeType: null,
|
|
},
|
|
],
|
|
suppressedMessages: [],
|
|
errorCount: 0,
|
|
warningCount: 1,
|
|
fatalErrorCount: 0,
|
|
fixableErrorCount: 0,
|
|
fixableWarningCount: 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* It will calculate the error and warning count for collection of messages per file
|
|
* @param {LintMessage[]} messages Collection of messages
|
|
* @returns {Object} Contains the stats
|
|
* @private
|
|
*/
|
|
function calculateStatsPerFile(messages) {
|
|
const stat = {
|
|
errorCount: 0,
|
|
fatalErrorCount: 0,
|
|
warningCount: 0,
|
|
fixableErrorCount: 0,
|
|
fixableWarningCount: 0,
|
|
};
|
|
|
|
for (let i = 0; i < messages.length; i++) {
|
|
const message = messages[i];
|
|
|
|
if (message.fatal || message.severity === 2) {
|
|
stat.errorCount++;
|
|
if (message.fatal) {
|
|
stat.fatalErrorCount++;
|
|
}
|
|
if (message.fix) {
|
|
stat.fixableErrorCount++;
|
|
}
|
|
} else {
|
|
stat.warningCount++;
|
|
if (message.fix) {
|
|
stat.fixableWarningCount++;
|
|
}
|
|
}
|
|
}
|
|
return stat;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Options-related Helpers
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Check if a given value is a valid fix type or not.
|
|
* @param {any} x The value to check.
|
|
* @returns {boolean} `true` if `x` is valid fix type.
|
|
*/
|
|
function isFixType(x) {
|
|
return (
|
|
x === "directive" ||
|
|
x === "problem" ||
|
|
x === "suggestion" ||
|
|
x === "layout"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if a given value is an array of fix types or not.
|
|
* @param {any} x The value to check.
|
|
* @returns {boolean} `true` if `x` is an array of fix types.
|
|
*/
|
|
function isFixTypeArray(x) {
|
|
return Array.isArray(x) && x.every(isFixType);
|
|
}
|
|
|
|
/**
|
|
* The error for invalid options.
|
|
*/
|
|
class ESLintInvalidOptionsError extends Error {
|
|
constructor(messages) {
|
|
super(`Invalid Options:\n- ${messages.join("\n- ")}`);
|
|
this.code = "ESLINT_INVALID_OPTIONS";
|
|
Error.captureStackTrace(this, ESLintInvalidOptionsError);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates and normalizes options for the wrapped CLIEngine instance.
|
|
* @param {ESLintOptions} options The options to process.
|
|
* @throws {ESLintInvalidOptionsError} If of any of a variety of type errors.
|
|
* @returns {ESLintOptions} The normalized options.
|
|
*/
|
|
function processOptions({
|
|
allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instead because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored.
|
|
baseConfig = null,
|
|
cache = false,
|
|
cacheLocation = ".eslintcache",
|
|
cacheStrategy = "metadata",
|
|
concurrency = "off",
|
|
cwd = process.cwd(),
|
|
errorOnUnmatchedPattern = true,
|
|
fix = false,
|
|
fixTypes = null, // ← should be null by default because if it's an array then it suppresses rules that don't have the `meta.type` property.
|
|
flags = [],
|
|
globInputPaths = true,
|
|
ignore = true,
|
|
ignorePatterns = null,
|
|
overrideConfig = null,
|
|
overrideConfigFile = null,
|
|
plugins = {},
|
|
stats = false,
|
|
warnIgnored = true,
|
|
passOnNoPatterns = false,
|
|
ruleFilter = () => true,
|
|
...unknownOptions
|
|
}) {
|
|
const errors = [];
|
|
const unknownOptionKeys = Object.keys(unknownOptions);
|
|
|
|
if (unknownOptionKeys.length >= 1) {
|
|
errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`);
|
|
if (unknownOptionKeys.includes("cacheFile")) {
|
|
errors.push(
|
|
"'cacheFile' has been removed. Please use the 'cacheLocation' option instead.",
|
|
);
|
|
}
|
|
if (unknownOptionKeys.includes("configFile")) {
|
|
errors.push(
|
|
"'configFile' has been removed. Please use the 'overrideConfigFile' option instead.",
|
|
);
|
|
}
|
|
if (unknownOptionKeys.includes("envs")) {
|
|
errors.push("'envs' has been removed.");
|
|
}
|
|
if (unknownOptionKeys.includes("extensions")) {
|
|
errors.push("'extensions' has been removed.");
|
|
}
|
|
if (unknownOptionKeys.includes("resolvePluginsRelativeTo")) {
|
|
errors.push("'resolvePluginsRelativeTo' has been removed.");
|
|
}
|
|
if (unknownOptionKeys.includes("globals")) {
|
|
errors.push(
|
|
"'globals' has been removed. Please use the 'overrideConfig.languageOptions.globals' option instead.",
|
|
);
|
|
}
|
|
if (unknownOptionKeys.includes("ignorePath")) {
|
|
errors.push("'ignorePath' has been removed.");
|
|
}
|
|
if (unknownOptionKeys.includes("ignorePattern")) {
|
|
errors.push(
|
|
"'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead.",
|
|
);
|
|
}
|
|
if (unknownOptionKeys.includes("parser")) {
|
|
errors.push(
|
|
"'parser' has been removed. Please use the 'overrideConfig.languageOptions.parser' option instead.",
|
|
);
|
|
}
|
|
if (unknownOptionKeys.includes("parserOptions")) {
|
|
errors.push(
|
|
"'parserOptions' has been removed. Please use the 'overrideConfig.languageOptions.parserOptions' option instead.",
|
|
);
|
|
}
|
|
if (unknownOptionKeys.includes("rules")) {
|
|
errors.push(
|
|
"'rules' has been removed. Please use the 'overrideConfig.rules' option instead.",
|
|
);
|
|
}
|
|
if (unknownOptionKeys.includes("rulePaths")) {
|
|
errors.push(
|
|
"'rulePaths' has been removed. Please define your rules using plugins.",
|
|
);
|
|
}
|
|
if (unknownOptionKeys.includes("reportUnusedDisableDirectives")) {
|
|
errors.push(
|
|
"'reportUnusedDisableDirectives' has been removed. Please use the 'overrideConfig.linterOptions.reportUnusedDisableDirectives' option instead.",
|
|
);
|
|
}
|
|
}
|
|
if (typeof allowInlineConfig !== "boolean") {
|
|
errors.push("'allowInlineConfig' must be a boolean.");
|
|
}
|
|
if (typeof baseConfig !== "object") {
|
|
errors.push("'baseConfig' must be an object or null.");
|
|
}
|
|
if (typeof cache !== "boolean") {
|
|
errors.push("'cache' must be a boolean.");
|
|
}
|
|
if (!isNonEmptyString(cacheLocation)) {
|
|
errors.push("'cacheLocation' must be a non-empty string.");
|
|
}
|
|
if (cacheStrategy !== "metadata" && cacheStrategy !== "content") {
|
|
errors.push('\'cacheStrategy\' must be any of "metadata", "content".');
|
|
}
|
|
if (
|
|
concurrency !== "off" &&
|
|
concurrency !== "auto" &&
|
|
!isPositiveInteger(concurrency)
|
|
) {
|
|
errors.push(
|
|
'\'concurrency\' must be a positive integer, "auto", or "off".',
|
|
);
|
|
}
|
|
if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) {
|
|
errors.push("'cwd' must be an absolute path.");
|
|
}
|
|
if (typeof errorOnUnmatchedPattern !== "boolean") {
|
|
errors.push("'errorOnUnmatchedPattern' must be a boolean.");
|
|
}
|
|
if (typeof fix !== "boolean" && typeof fix !== "function") {
|
|
errors.push("'fix' must be a boolean or a function.");
|
|
}
|
|
if (fixTypes !== null && !isFixTypeArray(fixTypes)) {
|
|
errors.push(
|
|
'\'fixTypes\' must be an array of any of "directive", "problem", "suggestion", and "layout".',
|
|
);
|
|
}
|
|
if (!isEmptyArrayOrArrayOfNonEmptyString(flags)) {
|
|
errors.push("'flags' must be an array of non-empty strings.");
|
|
}
|
|
if (typeof globInputPaths !== "boolean") {
|
|
errors.push("'globInputPaths' must be a boolean.");
|
|
}
|
|
if (typeof ignore !== "boolean") {
|
|
errors.push("'ignore' must be a boolean.");
|
|
}
|
|
if (
|
|
!isEmptyArrayOrArrayOfNonEmptyString(ignorePatterns) &&
|
|
ignorePatterns !== null
|
|
) {
|
|
errors.push(
|
|
"'ignorePatterns' must be an array of non-empty strings or null.",
|
|
);
|
|
}
|
|
if (typeof overrideConfig !== "object") {
|
|
errors.push("'overrideConfig' must be an object or null.");
|
|
}
|
|
if (
|
|
!isNonEmptyString(overrideConfigFile) &&
|
|
overrideConfigFile !== null &&
|
|
overrideConfigFile !== true
|
|
) {
|
|
errors.push(
|
|
"'overrideConfigFile' must be a non-empty string, null, or true.",
|
|
);
|
|
}
|
|
if (typeof passOnNoPatterns !== "boolean") {
|
|
errors.push("'passOnNoPatterns' must be a boolean.");
|
|
}
|
|
if (typeof plugins !== "object") {
|
|
errors.push("'plugins' must be an object or null.");
|
|
} else if (plugins !== null && Object.keys(plugins).includes("")) {
|
|
errors.push("'plugins' must not include an empty string.");
|
|
}
|
|
if (Array.isArray(plugins)) {
|
|
errors.push(
|
|
"'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead.",
|
|
);
|
|
}
|
|
if (typeof stats !== "boolean") {
|
|
errors.push("'stats' must be a boolean.");
|
|
}
|
|
if (typeof warnIgnored !== "boolean") {
|
|
errors.push("'warnIgnored' must be a boolean.");
|
|
}
|
|
if (typeof ruleFilter !== "function") {
|
|
errors.push("'ruleFilter' must be a function.");
|
|
}
|
|
if (errors.length > 0) {
|
|
throw new ESLintInvalidOptionsError(errors);
|
|
}
|
|
|
|
return {
|
|
allowInlineConfig,
|
|
baseConfig,
|
|
cache,
|
|
cacheLocation,
|
|
cacheStrategy,
|
|
concurrency,
|
|
|
|
// when overrideConfigFile is true that means don't do config file lookup
|
|
configFile: overrideConfigFile === true ? false : overrideConfigFile,
|
|
overrideConfig,
|
|
cwd: path.normalize(cwd),
|
|
errorOnUnmatchedPattern,
|
|
fix,
|
|
fixTypes,
|
|
flags: [...flags],
|
|
globInputPaths,
|
|
ignore,
|
|
ignorePatterns,
|
|
stats,
|
|
passOnNoPatterns,
|
|
warnIgnored,
|
|
ruleFilter,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Loads ESLint constructor options from an options module.
|
|
* @param {string} optionsURL The URL string of the options module to load.
|
|
* @returns {Promise<ESLintOptions>} ESLint constructor options.
|
|
*/
|
|
async function loadOptionsFromModule(optionsURL) {
|
|
return (await import(optionsURL)).default;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Cache-related helpers
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* return the cacheFile to be used by eslint, based on whether the provided parameter is
|
|
* a directory or looks like a directory (ends in `path.sep`), in which case the file
|
|
* name will be the `cacheFile/.cache_hashOfCWD`
|
|
*
|
|
* if cacheFile points to a file or looks like a file then in will just use that file
|
|
* @param {string} cacheFile The name of file to be used to store the cache
|
|
* @param {string} cwd Current working directory
|
|
* @param {Object} options The options
|
|
* @param {string} [options.prefix] The prefix to use for the cache file
|
|
* @returns {string} the resolved path to the cache file
|
|
*/
|
|
function getCacheFile(cacheFile, cwd, { prefix = ".cache_" } = {}) {
|
|
/*
|
|
* make sure the path separators are normalized for the environment/os
|
|
* keeping the trailing path separator if present
|
|
*/
|
|
const normalizedCacheFile = path.normalize(cacheFile);
|
|
|
|
const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile);
|
|
const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep;
|
|
|
|
/**
|
|
* return the name for the cache file in case the provided parameter is a directory
|
|
* @returns {string} the resolved path to the cacheFile
|
|
*/
|
|
function getCacheFileForDirectory() {
|
|
return path.join(resolvedCacheFile, `${prefix}${hash(cwd)}`);
|
|
}
|
|
|
|
let fileStats;
|
|
|
|
try {
|
|
fileStats = fs.lstatSync(resolvedCacheFile);
|
|
} catch {
|
|
fileStats = null;
|
|
}
|
|
|
|
/*
|
|
* in case the file exists we need to verify if the provided path
|
|
* is a directory or a file. If it is a directory we want to create a file
|
|
* inside that directory
|
|
*/
|
|
if (fileStats) {
|
|
/*
|
|
* is a directory or is a file, but the original file the user provided
|
|
* looks like a directory but `path.resolve` removed the `last path.sep`
|
|
* so we need to still treat this like a directory
|
|
*/
|
|
if (fileStats.isDirectory() || looksLikeADirectory) {
|
|
return getCacheFileForDirectory();
|
|
}
|
|
|
|
// is file so just use that file
|
|
return resolvedCacheFile;
|
|
}
|
|
|
|
/*
|
|
* here we known the file or directory doesn't exist,
|
|
* so we will try to infer if its a directory if it looks like a directory
|
|
* for the current operating system.
|
|
*/
|
|
|
|
// if the last character passed is a path separator we assume is a directory
|
|
if (looksLikeADirectory) {
|
|
return getCacheFileForDirectory();
|
|
}
|
|
|
|
return resolvedCacheFile;
|
|
}
|
|
|
|
/**
|
|
* Creates a new lint result cache.
|
|
* @param {ESLintOptions} eslintOptions The processed ESLint options.
|
|
* @param {string} cacheFilePath The path to the cache file.
|
|
* @returns {?LintResultCache} A new lint result cache or `null`.
|
|
*/
|
|
function createLintResultCache({ cache, cacheStrategy }, cacheFilePath) {
|
|
return cache ? new LintResultCache(cacheFilePath, cacheStrategy) : null;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Lint helpers
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Checks whether a message's rule type should be fixed.
|
|
* @param {LintMessage} message The message to check.
|
|
* @param {CalculatedConfig} config The config for the file that generated the message.
|
|
* @param {string[]} fixTypes An array of fix types to check.
|
|
* @returns {boolean} Whether the message should be fixed.
|
|
*/
|
|
function shouldMessageBeFixed(message, config, fixTypes) {
|
|
if (!message.ruleId) {
|
|
return fixTypes.has("directive");
|
|
}
|
|
|
|
const rule = message.ruleId && config.getRuleDefinition(message.ruleId);
|
|
|
|
return Boolean(rule && rule.meta && fixTypes.has(rule.meta.type));
|
|
}
|
|
|
|
/**
|
|
* Creates a fixer function based on the provided fix, fixTypesSet, and config.
|
|
* @param {Function|boolean} fix The original fix option.
|
|
* @param {Set<string>} fixTypesSet A set of fix types to filter messages for fixing.
|
|
* @param {CalculatedConfig} config The config for the file that generated the message.
|
|
* @returns {Function|boolean} The fixer function or the original fix value.
|
|
*/
|
|
function getFixerForFixTypes(fix, fixTypesSet, config) {
|
|
if (!fix || !fixTypesSet) {
|
|
return fix;
|
|
}
|
|
|
|
const originalFix = typeof fix === "function" ? fix : () => true;
|
|
|
|
return message =>
|
|
shouldMessageBeFixed(message, config, fixTypesSet) &&
|
|
originalFix(message);
|
|
}
|
|
|
|
/**
|
|
* Processes a source code using ESLint.
|
|
* @param {Object} config The config object.
|
|
* @param {string} config.text The source code to verify.
|
|
* @param {string} config.cwd The path to the current working directory.
|
|
* @param {string|undefined} config.filePath The path to the file of `text`. If this is undefined, it uses `<text>`.
|
|
* @param {FlatConfigArray} config.configs The config.
|
|
* @param {boolean} config.fix If `true` then it does fix.
|
|
* @param {boolean} config.allowInlineConfig If `true` then it uses directive comments.
|
|
* @param {Function} config.ruleFilter A predicate function to filter which rules should be run.
|
|
* @param {boolean} config.stats If `true`, then if reports extra statistics with the lint results.
|
|
* @param {Linter} config.linter The linter instance to verify.
|
|
* @returns {LintResult} The result of linting.
|
|
* @private
|
|
*/
|
|
function verifyText({
|
|
text,
|
|
cwd,
|
|
filePath: providedFilePath,
|
|
configs,
|
|
fix,
|
|
allowInlineConfig,
|
|
ruleFilter,
|
|
stats,
|
|
linter,
|
|
}) {
|
|
const startTime = hrtimeBigint();
|
|
|
|
const filePath = providedFilePath || "<text>";
|
|
|
|
/*
|
|
* Verify.
|
|
* `config.extractConfig(filePath)` requires an absolute path, but `linter`
|
|
* doesn't know CWD, so it gives `linter` an absolute path always.
|
|
*/
|
|
const filePathToVerify =
|
|
filePath === "<text>" ? getPlaceholderPath(cwd) : filePath;
|
|
const { fixed, messages, output } = linter.verifyAndFix(text, configs, {
|
|
allowInlineConfig,
|
|
filename: filePathToVerify,
|
|
fix,
|
|
ruleFilter,
|
|
stats,
|
|
|
|
/**
|
|
* Check if the linter should adopt a given code block or not.
|
|
* @param {string} blockFilename The virtual filename of a code block.
|
|
* @returns {boolean} `true` if the linter should adopt the code block.
|
|
*/
|
|
filterCodeBlock(blockFilename) {
|
|
return configs.getConfig(blockFilename) !== void 0;
|
|
},
|
|
});
|
|
|
|
// Tweak and return.
|
|
const result = {
|
|
filePath: filePath === "<text>" ? filePath : path.resolve(filePath),
|
|
messages,
|
|
suppressedMessages: linter.getSuppressedMessages(),
|
|
...calculateStatsPerFile(messages),
|
|
};
|
|
|
|
if (fixed) {
|
|
result.output = output;
|
|
}
|
|
|
|
if (
|
|
result.errorCount + result.warningCount > 0 &&
|
|
typeof result.output === "undefined"
|
|
) {
|
|
result.source = text;
|
|
}
|
|
|
|
if (stats) {
|
|
result.stats = {
|
|
times: linter.getTimes(),
|
|
fixPasses: linter.getFixPassCount(),
|
|
};
|
|
}
|
|
|
|
const endTime = hrtimeBigint();
|
|
debug('File "%s" linted in %t', filePath, endTime - startTime);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Lints a single file.
|
|
* @param {string} filePath File path to lint.
|
|
* @param {FlatConfigArray} configs The config array for the file.
|
|
* @param {ESLintOptions} eslintOptions The processed ESLint options.
|
|
* @param {Linter} linter The linter instance to use.
|
|
* @param {?LintResultCache} lintResultCache The result cache or `null`.
|
|
* @param {?{ duration: bigint; }} readFileCounter Used to keep track of the time spent reading files.
|
|
* @param {Retrier} [retrier] Used to retry linting on certain errors.
|
|
* @param {AbortController} [controller] Used to stop linting when an error occurs.
|
|
* @returns {Promise<LintResult>} The lint result.
|
|
*/
|
|
async function lintFile(
|
|
filePath,
|
|
configs,
|
|
eslintOptions,
|
|
linter,
|
|
lintResultCache,
|
|
readFileCounter,
|
|
retrier,
|
|
controller,
|
|
) {
|
|
const config = configs.getConfig(filePath);
|
|
const {
|
|
allowInlineConfig,
|
|
cwd,
|
|
fix,
|
|
fixTypes,
|
|
ruleFilter,
|
|
stats,
|
|
warnIgnored,
|
|
} = eslintOptions;
|
|
const fixTypesSet = fixTypes ? new Set(fixTypes) : null;
|
|
|
|
/*
|
|
* If a filename was entered that cannot be matched
|
|
* to a config, then notify the user.
|
|
*/
|
|
if (!config) {
|
|
if (warnIgnored) {
|
|
const configStatus = configs.getConfigStatus(filePath);
|
|
|
|
return createIgnoreResult(filePath, cwd, configStatus);
|
|
}
|
|
|
|
return void 0;
|
|
}
|
|
|
|
// Skip if there is cached result.
|
|
if (lintResultCache) {
|
|
const cachedResult = lintResultCache.getCachedLintResults(
|
|
filePath,
|
|
config,
|
|
);
|
|
|
|
if (cachedResult) {
|
|
const hadMessages =
|
|
cachedResult.messages && cachedResult.messages.length > 0;
|
|
|
|
if (hadMessages && fix) {
|
|
debug(`Reprocessing cached file to allow autofix: ${filePath}`);
|
|
} else {
|
|
debug(`Skipping file since it hasn't changed: ${filePath}`);
|
|
return cachedResult;
|
|
}
|
|
}
|
|
}
|
|
|
|
// set up fixer for fixTypes if necessary
|
|
const fixer = getFixerForFixTypes(fix, fixTypesSet, config);
|
|
|
|
/**
|
|
* Reads the file and lints its content.
|
|
* @returns {Promise<LintResult>} A lint result.
|
|
*/
|
|
async function readAndVerifyFile() {
|
|
const readFileEnterTime = hrtimeBigint();
|
|
const text = await fsp.readFile(filePath, {
|
|
encoding: "utf8",
|
|
signal: controller?.signal,
|
|
});
|
|
const readFileExitTime = hrtimeBigint();
|
|
const readFileDuration = readFileExitTime - readFileEnterTime;
|
|
debug('File "%s" read in %t', filePath, readFileDuration);
|
|
if (readFileCounter) {
|
|
readFileCounter.duration += readFileDuration;
|
|
}
|
|
|
|
// fail immediately if an error occurred in another file
|
|
controller?.signal.throwIfAborted();
|
|
|
|
// do the linting
|
|
return verifyText({
|
|
text,
|
|
filePath,
|
|
configs,
|
|
cwd,
|
|
fix: fixer,
|
|
allowInlineConfig,
|
|
ruleFilter,
|
|
stats,
|
|
linter,
|
|
});
|
|
}
|
|
|
|
// Use the retrier if provided, otherwise just call the function.
|
|
const readAndVerifyFilePromise = retrier
|
|
? retrier.retry(readAndVerifyFile, { signal: controller?.signal })
|
|
: readAndVerifyFile();
|
|
|
|
return readAndVerifyFilePromise.catch(error => {
|
|
controller?.abort(error);
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retrieves flags from the environment variable ESLINT_FLAGS.
|
|
* @param {string[]} flags The flags defined via the API.
|
|
* @returns {string[]} The merged flags to use.
|
|
*/
|
|
function mergeEnvironmentFlags(flags) {
|
|
if (!process.env.ESLINT_FLAGS) {
|
|
return flags;
|
|
}
|
|
|
|
const envFlags = process.env.ESLINT_FLAGS.trim().split(/\s*,\s*/gu);
|
|
return Array.from(new Set([...envFlags, ...flags]));
|
|
}
|
|
|
|
/**
|
|
* Creates a new linter instance.
|
|
* @param {ESLintOptions} eslintOptions The processed ESLint options.
|
|
* @param {WarningService} warningService The warning service to use.
|
|
* @returns {Linter} The linter instance.
|
|
*/
|
|
function createLinter({ cwd, flags }, warningService) {
|
|
return new Linter({
|
|
configType: "flat",
|
|
cwd,
|
|
flags: mergeEnvironmentFlags(flags),
|
|
warningService,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates default configs with the specified plugins.
|
|
* @param {Record<string, Plugin> | undefined} optionPlugins The plugins specified in the ESLint options.
|
|
* @returns {Config[]} The default configs.
|
|
*/
|
|
function createDefaultConfigs(optionPlugins) {
|
|
const defaultConfigs = [];
|
|
|
|
// Add plugins
|
|
if (optionPlugins) {
|
|
const plugins = {};
|
|
|
|
for (const [pluginName, plugin] of Object.entries(optionPlugins)) {
|
|
plugins[getShorthandName(pluginName, "eslint-plugin")] = plugin;
|
|
}
|
|
|
|
defaultConfigs.push({ plugins });
|
|
}
|
|
|
|
return defaultConfigs;
|
|
}
|
|
|
|
/**
|
|
* Creates a config loader.
|
|
* @param {ESLintOptions} eslintOptions The processed ESLint options.
|
|
* @param {Config[]} defaultConfigs The default configs.
|
|
* @param {Linter} linter The linter instance.
|
|
* @param {WarningService} warningService The warning service to use.
|
|
* @returns {ConfigLoader} The config loader.
|
|
*/
|
|
function createConfigLoader(
|
|
{
|
|
cwd,
|
|
baseConfig,
|
|
overrideConfig,
|
|
configFile,
|
|
ignore: ignoreEnabled,
|
|
ignorePatterns,
|
|
},
|
|
defaultConfigs,
|
|
linter,
|
|
warningService,
|
|
) {
|
|
const configLoaderOptions = {
|
|
cwd,
|
|
baseConfig,
|
|
overrideConfig,
|
|
configFile,
|
|
ignoreEnabled,
|
|
ignorePatterns,
|
|
defaultConfigs,
|
|
hasUnstableNativeNodeJsTSConfigFlag: linter.hasFlag(
|
|
"unstable_native_nodejs_ts_config",
|
|
),
|
|
warningService,
|
|
};
|
|
|
|
return linter.hasFlag("v10_config_lookup_from_file")
|
|
? new ConfigLoader(configLoaderOptions)
|
|
: new LegacyConfigLoader(configLoaderOptions);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Exports
|
|
//-----------------------------------------------------------------------------
|
|
|
|
module.exports = {
|
|
createDebug,
|
|
|
|
findFiles,
|
|
|
|
isNonEmptyString,
|
|
isArrayOfNonEmptyString,
|
|
|
|
createIgnoreResult,
|
|
isErrorMessage,
|
|
calculateStatsPerFile,
|
|
getPlaceholderPath,
|
|
|
|
processOptions,
|
|
loadOptionsFromModule,
|
|
|
|
getCacheFile,
|
|
createLintResultCache,
|
|
|
|
getFixerForFixTypes,
|
|
verifyText,
|
|
lintFile,
|
|
createLinter,
|
|
createDefaultConfigs,
|
|
createConfigLoader,
|
|
};
|