eslint
Advanced tools
Comparing version 8.56.0 to 9.0.0-alpha.0
@@ -26,4 +26,6 @@ { | ||
{ "removed": "space-unary-word-ops", "replacedBy": ["space-unary-ops"] }, | ||
{ "removed": "spaced-line-comment", "replacedBy": ["spaced-comment"] } | ||
{ "removed": "spaced-line-comment", "replacedBy": ["spaced-comment"] }, | ||
{ "removed": "valid-jsdoc", "replacedBy": [] }, | ||
{ "removed": "require-jsdoc", "replacedBy": [] } | ||
] | ||
} |
@@ -12,3 +12,3 @@ /** | ||
const { ESLint } = require("./eslint"); | ||
const { ESLint } = require("./eslint/eslint"); | ||
const { Linter } = require("./linter"); | ||
@@ -15,0 +15,0 @@ const { RuleTester } = require("./rule-tester"); |
@@ -44,2 +44,13 @@ /** | ||
const debug = require("debug")("eslint:cli-engine"); | ||
const removedFormatters = new Set([ | ||
"checkstyle", | ||
"codeframe", | ||
"compact", | ||
"jslint-xml", | ||
"junit", | ||
"table", | ||
"tap", | ||
"unix", | ||
"visualstudio" | ||
]); | ||
const validFixTypes = new Set(["directive", "problem", "suggestion", "layout"]); | ||
@@ -643,3 +654,3 @@ | ||
options.cache ? new LintResultCache(cacheFilePath, options.cacheStrategy) : null; | ||
const linter = new Linter({ cwd: options.cwd }); | ||
const linter = new Linter({ cwd: options.cwd, configType: "eslintrc" }); | ||
@@ -1052,3 +1063,3 @@ /** @type {ConfigArray[]} */ | ||
} catch (ex) { | ||
if (format === "table" || format === "codeframe") { | ||
if (removedFormatters.has(format)) { | ||
ex.message = `The ${format} formatter is no longer part of core ESLint. Install it manually with \`npm install -D eslint-formatter-${format}\``; | ||
@@ -1055,0 +1066,0 @@ } else { |
[ | ||
{ | ||
"name": "checkstyle", | ||
"description": "Outputs results to the [Checkstyle](https://checkstyle.sourceforge.io/) format." | ||
}, | ||
{ | ||
"name": "compact", | ||
"description": "Human-readable output format. Mimics the default output of JSHint." | ||
}, | ||
{ | ||
"name": "html", | ||
@@ -15,6 +7,2 @@ "description": "Outputs results to HTML. The `html` formatter is useful for visual presentation in the browser." | ||
{ | ||
"name": "jslint-xml", | ||
"description": "Outputs results to format compatible with the [JSLint Jenkins plugin](https://plugins.jenkins.io/jslint/)." | ||
}, | ||
{ | ||
"name": "json-with-metadata", | ||
@@ -28,21 +16,5 @@ "description": "Outputs JSON-serialized results. The `json-with-metadata` provides the same linting results as the [`json`](#json) formatter with additional metadata about the rules applied. The linting results are included in the `results` property and the rules metadata is included in the `metadata` property.\n\nAlternatively, you can use the [ESLint Node.js API](../../integrate/nodejs-api) to programmatically use ESLint." | ||
{ | ||
"name": "junit", | ||
"description": "Outputs results to format compatible with the [JUnit Jenkins plugin](https://plugins.jenkins.io/junit/)." | ||
}, | ||
{ | ||
"name": "stylish", | ||
"description": "Human-readable output format. This is the default formatter." | ||
}, | ||
{ | ||
"name": "tap", | ||
"description": "Outputs results to the [Test Anything Protocol (TAP)](https://testanything.org/) specification format." | ||
}, | ||
{ | ||
"name": "unix", | ||
"description": "Outputs results to a format similar to many commands in UNIX-like systems. Parsable with tools such as [grep](https://www.gnu.org/software/grep/manual/grep.html), [sed](https://www.gnu.org/software/sed/manual/sed.html), and [awk](https://www.gnu.org/software/gawk/manual/gawk.html)." | ||
}, | ||
{ | ||
"name": "visualstudio", | ||
"description": "Outputs results to format compatible with the integrated terminal of the [Visual Studio](https://visualstudio.microsoft.com/) IDE. When using Visual Studio, you can click on the linting results in the integrated terminal to go to the issue in the source code." | ||
} | ||
] | ||
] |
@@ -21,4 +21,4 @@ /** | ||
{ promisify } = require("util"), | ||
{ ESLint } = require("./eslint"), | ||
{ FlatESLint, shouldUseFlatConfig } = require("./eslint/flat-eslint"), | ||
{ LegacyESLint } = require("./eslint"), | ||
{ ESLint, shouldUseFlatConfig } = require("./eslint/eslint"), | ||
createCLIOptions = require("./options"), | ||
@@ -63,2 +63,12 @@ log = require("./shared/logging"), | ||
/** | ||
* Predicate function for whether or not to run a rule in quiet mode. | ||
* If a rule is set to warning, do not run it. | ||
* @param {{ ruleId: string; severity: number; }} rule The rule id and severity. | ||
* @returns {boolean} True if the lint rule should run, false otherwise. | ||
*/ | ||
function quietRuleFilter(rule) { | ||
return rule.severity === 2; | ||
} | ||
/** | ||
* Translates the CLI options into the options expected by the ESLint constructor. | ||
@@ -99,3 +109,5 @@ * @param {ParsedCLIOptions} cliOptions The CLI options to translate. | ||
rulesdir, | ||
warnIgnored | ||
warnIgnored, | ||
passOnNoPatterns, | ||
maxWarnings | ||
}, configType) { | ||
@@ -193,3 +205,4 @@ | ||
overrideConfig, | ||
overrideConfigFile | ||
overrideConfigFile, | ||
passOnNoPatterns | ||
}; | ||
@@ -200,2 +213,8 @@ | ||
options.warnIgnored = warnIgnored; | ||
/* | ||
* For performance reasons rules not marked as 'error' are filtered out in quiet mode. As maxWarnings | ||
* requires rules set to 'warn' to be run, we only filter out 'warn' rules if maxWarnings is not specified. | ||
*/ | ||
options.ruleFilter = quiet && maxWarnings === -1 ? quietRuleFilter : () => true; | ||
} else { | ||
@@ -312,6 +331,6 @@ options.resolvePluginsRelativeTo = resolvePluginsRelativeTo; | ||
* @param {string} [text] The text to lint (used for TTY). | ||
* @param {boolean} [allowFlatConfig] Whether or not to allow flat config. | ||
* @param {boolean} [allowFlatConfig=true] Whether or not to allow flat config. | ||
* @returns {Promise<number>} The exit code for the operation. | ||
*/ | ||
async execute(args, text, allowFlatConfig) { | ||
async execute(args, text, allowFlatConfig = true) { | ||
if (Array.isArray(args)) { | ||
@@ -332,2 +351,6 @@ debug("CLI args: %o", args.slice(2)); | ||
if (allowFlatConfig && !usingFlatConfig) { | ||
process.emitWarning("You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details.", "ESLintRCWarning"); | ||
} | ||
const CLIOptions = createCLIOptions(usingFlatConfig); | ||
@@ -386,4 +409,4 @@ | ||
const engine = usingFlatConfig | ||
? new FlatESLint(await translateOptions(options, "flat")) | ||
: new ESLint(await translateOptions(options)); | ||
? new ESLint(await translateOptions(options, "flat")) | ||
: new LegacyESLint(await translateOptions(options)); | ||
const fileConfig = | ||
@@ -416,3 +439,3 @@ await engine.calculateConfigForFile(options.printConfig); | ||
const ActiveESLint = usingFlatConfig ? FlatESLint : ESLint; | ||
const ActiveESLint = usingFlatConfig ? ESLint : LegacyESLint; | ||
@@ -419,0 +442,0 @@ const engine = new ActiveESLint(await translateOptions(options, usingFlatConfig ? "flat" : "eslintrc")); |
@@ -45,2 +45,5 @@ /** | ||
parserOptions: {} | ||
}, | ||
linterOptions: { | ||
reportUnusedDisableDirectives: 1 | ||
} | ||
@@ -47,0 +50,0 @@ }, |
@@ -16,3 +16,2 @@ /** | ||
const { defaultConfig } = require("./default-config"); | ||
const jsPlugin = require("@eslint/js"); | ||
@@ -138,22 +137,3 @@ //----------------------------------------------------------------------------- | ||
[ConfigArraySymbol.preprocessConfig](config) { | ||
if (config === "eslint:recommended") { | ||
// if we are in a Node.js environment warn the user | ||
if (typeof process !== "undefined" && process.emitWarning) { | ||
process.emitWarning("The 'eslint:recommended' string configuration is deprecated and will be replaced by the @eslint/js package's 'recommended' config."); | ||
} | ||
return jsPlugin.configs.recommended; | ||
} | ||
if (config === "eslint:all") { | ||
// if we are in a Node.js environment warn the user | ||
if (typeof process !== "undefined" && process.emitWarning) { | ||
process.emitWarning("The 'eslint:all' string configuration is deprecated and will be replaced by the @eslint/js package's 'all' config."); | ||
} | ||
return jsPlugin.configs.all; | ||
} | ||
/* | ||
@@ -160,0 +140,0 @@ * If `shouldIgnore` is false, we remove any ignore patterns specified |
@@ -8,2 +8,19 @@ /** | ||
//------------------------------------------------------------------------------ | ||
// Typedefs | ||
//------------------------------------------------------------------------------ | ||
/** @typedef {import("../shared/types").Rule} Rule */ | ||
//------------------------------------------------------------------------------ | ||
// Private Members | ||
//------------------------------------------------------------------------------ | ||
// JSON schema that disallows passing any options | ||
const noOptionsSchema = Object.freeze({ | ||
type: "array", | ||
minItems: 0, | ||
maxItems: 0 | ||
}); | ||
//----------------------------------------------------------------------------- | ||
@@ -56,12 +73,4 @@ // Functions | ||
const plugin = config.plugins && config.plugins[pluginName]; | ||
let rule = plugin && plugin.rules && plugin.rules[ruleName]; | ||
const rule = plugin && plugin.rules && plugin.rules[ruleName]; | ||
// normalize function rules into objects | ||
if (rule && typeof rule === "function") { | ||
rule = { | ||
create: rule | ||
}; | ||
} | ||
return rule; | ||
@@ -72,13 +81,28 @@ } | ||
* Gets a complete options schema for a rule. | ||
* @param {{create: Function, schema: (Array|null)}} rule A new-style rule object | ||
* @returns {Object} JSON Schema for the rule's options. | ||
* @param {Rule} rule A rule object | ||
* @throws {TypeError} If `meta.schema` is specified but is not an array, object or `false`. | ||
* @returns {Object|null} JSON Schema for the rule's options. `null` if `meta.schema` is `false`. | ||
*/ | ||
function getRuleOptionsSchema(rule) { | ||
if (!rule) { | ||
if (!rule.meta) { | ||
return { ...noOptionsSchema }; // default if `meta.schema` is not specified | ||
} | ||
const schema = rule.meta.schema; | ||
if (typeof schema === "undefined") { | ||
return { ...noOptionsSchema }; // default if `meta.schema` is not specified | ||
} | ||
// `schema:false` is an allowed explicit opt-out of options validation for the rule | ||
if (schema === false) { | ||
return null; | ||
} | ||
const schema = rule.schema || rule.meta && rule.meta.schema; | ||
if (typeof schema !== "object" || schema === null) { | ||
throw new TypeError("Rule's `meta.schema` must be an array or object"); | ||
} | ||
// ESLint-specific array form needs to be converted into a valid JSON Schema definition | ||
if (Array.isArray(schema)) { | ||
@@ -93,12 +117,9 @@ if (schema.length) { | ||
} | ||
return { | ||
type: "array", | ||
minItems: 0, | ||
maxItems: 0 | ||
}; | ||
// `schema:[]` is an explicit way to specify that the rule does not accept any options | ||
return { ...noOptionsSchema }; | ||
} | ||
// Given a full schema, leave it alone | ||
return schema || null; | ||
// `schema:<object>` is assumed to be a valid JSON Schema definition | ||
return schema; | ||
} | ||
@@ -105,0 +126,0 @@ |
@@ -12,7 +12,2 @@ /** | ||
/* | ||
* Note: This can be removed in ESLint v9 because structuredClone is available globally | ||
* starting in Node.js v17. | ||
*/ | ||
const structuredClone = require("@ungap/structured-clone").default; | ||
const { normalizeSeverityToNumber } = require("../shared/severity"); | ||
@@ -58,2 +53,11 @@ | ||
/** | ||
* Check if a value is a non-null non-array object. | ||
* @param {any} value The value to check. | ||
* @returns {boolean} `true` if the value is a non-null non-array object. | ||
*/ | ||
function isNonArrayObject(value) { | ||
return isNonNullObject(value) && !Array.isArray(value); | ||
} | ||
/** | ||
* Check if a value is undefined. | ||
@@ -68,15 +72,23 @@ * @param {any} value The value to check. | ||
/** | ||
* Deeply merges two objects. | ||
* Deeply merges two non-array objects. | ||
* @param {Object} first The base object. | ||
* @param {Object} second The overrides object. | ||
* @param {Map<string, Map<string, Object>>} [mergeMap] Maps the combination of first and second arguments to a merged result. | ||
* @returns {Object} An object with properties from both first and second. | ||
*/ | ||
function deepMerge(first = {}, second = {}) { | ||
function deepMerge(first, second, mergeMap = new Map()) { | ||
/* | ||
* If the second value is an array, just return it. We don't merge | ||
* arrays because order matters and we can't know the correct order. | ||
*/ | ||
if (Array.isArray(second)) { | ||
return second; | ||
let secondMergeMap = mergeMap.get(first); | ||
if (secondMergeMap) { | ||
const result = secondMergeMap.get(second); | ||
if (result) { | ||
// If this combination of first and second arguments has been already visited, return the previously created result. | ||
return result; | ||
} | ||
} else { | ||
secondMergeMap = new Map(); | ||
mergeMap.set(first, secondMergeMap); | ||
} | ||
@@ -95,6 +107,11 @@ | ||
delete result.__proto__; // eslint-disable-line no-proto -- don't merge own property "__proto__" | ||
// Store the pending result for this combination of first and second arguments. | ||
secondMergeMap.set(second, result); | ||
for (const key of Object.keys(second)) { | ||
// avoid hairy edge case | ||
if (key === "__proto__") { | ||
if (key === "__proto__" || !Object.prototype.propertyIsEnumerable.call(first, key)) { | ||
continue; | ||
@@ -106,13 +123,6 @@ } | ||
if (isNonNullObject(firstValue)) { | ||
result[key] = deepMerge(firstValue, secondValue); | ||
} else if (isUndefined(firstValue)) { | ||
if (isNonNullObject(secondValue)) { | ||
result[key] = deepMerge( | ||
Array.isArray(secondValue) ? [] : {}, | ||
secondValue | ||
); | ||
} else if (!isUndefined(secondValue)) { | ||
result[key] = secondValue; | ||
} | ||
if (isNonArrayObject(firstValue) && isNonArrayObject(secondValue)) { | ||
result[key] = deepMerge(firstValue, secondValue, mergeMap); | ||
} else if (isUndefined(secondValue)) { | ||
result[key] = firstValue; | ||
} | ||
@@ -119,0 +129,0 @@ } |
@@ -69,2 +69,21 @@ /** | ||
/** | ||
* The error type when a rule has an invalid `meta.schema`. | ||
*/ | ||
class InvalidRuleOptionsSchemaError extends Error { | ||
/** | ||
* Creates a new instance. | ||
* @param {string} ruleId Id of the rule that has an invalid `meta.schema`. | ||
* @param {Error} processingError Error caught while processing the `meta.schema`. | ||
*/ | ||
constructor(ruleId, processingError) { | ||
super( | ||
`Error while processing options validation schema of rule '${ruleId}': ${processingError.message}`, | ||
{ cause: processingError } | ||
); | ||
this.code = "ESLINT_INVALID_RULE_OPTIONS_SCHEMA"; | ||
} | ||
} | ||
//----------------------------------------------------------------------------- | ||
@@ -134,6 +153,10 @@ // Exports | ||
if (!this.validators.has(rule)) { | ||
const schema = getRuleOptionsSchema(rule); | ||
try { | ||
const schema = getRuleOptionsSchema(rule); | ||
if (schema) { | ||
this.validators.set(rule, ajv.compile(schema)); | ||
if (schema) { | ||
this.validators.set(rule, ajv.compile(schema)); | ||
} | ||
} catch (err) { | ||
throw new InvalidRuleOptionsSchemaError(ruleId, err); | ||
} | ||
@@ -140,0 +163,0 @@ } |
@@ -108,7 +108,7 @@ /** | ||
* Check if a given value is a non-empty string or not. | ||
* @param {any} x The value to check. | ||
* @returns {boolean} `true` if `x` is a non-empty string. | ||
* @param {any} value The value to check. | ||
* @returns {boolean} `true` if `value` is a non-empty string. | ||
*/ | ||
function isNonEmptyString(x) { | ||
return typeof x === "string" && x.trim() !== ""; | ||
function isNonEmptyString(value) { | ||
return typeof value === "string" && value.trim() !== ""; | ||
} | ||
@@ -118,9 +118,19 @@ | ||
* Check if a given value is an array of non-empty strings or not. | ||
* @param {any} x The value to check. | ||
* @returns {boolean} `true` if `x` is an array of non-empty strings. | ||
* @param {any} value The value to check. | ||
* @returns {boolean} `true` if `value` is an array of non-empty strings. | ||
*/ | ||
function isArrayOfNonEmptyString(x) { | ||
return Array.isArray(x) && x.every(isNonEmptyString); | ||
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); | ||
} | ||
//----------------------------------------------------------------------------- | ||
@@ -660,5 +670,5 @@ // File-related Helpers | ||
* Validates and normalizes options for the wrapped CLIEngine instance. | ||
* @param {FlatESLintOptions} options The options to process. | ||
* @param {ESLintOptions} options The options to process. | ||
* @throws {ESLintInvalidOptionsError} If of any of a variety of type errors. | ||
* @returns {FlatESLintOptions} The normalized options. | ||
* @returns {ESLintOptions} The normalized options. | ||
*/ | ||
@@ -682,2 +692,4 @@ function processOptions({ | ||
warnIgnored = true, | ||
passOnNoPatterns = false, | ||
ruleFilter = () => true, | ||
...unknownOptions | ||
@@ -766,3 +778,3 @@ }) { | ||
} | ||
if (!isArrayOfNonEmptyString(ignorePatterns) && ignorePatterns !== null) { | ||
if (!isEmptyArrayOrArrayOfNonEmptyString(ignorePatterns) && ignorePatterns !== null) { | ||
errors.push("'ignorePatterns' must be an array of non-empty strings or null."); | ||
@@ -776,2 +788,5 @@ } | ||
} | ||
if (typeof passOnNoPatterns !== "boolean") { | ||
errors.push("'passOnNoPatterns' must be a boolean."); | ||
} | ||
if (typeof plugins !== "object") { | ||
@@ -788,2 +803,5 @@ errors.push("'plugins' must be an object or null."); | ||
} | ||
if (typeof ruleFilter !== "function") { | ||
errors.push("'ruleFilter' must be a function."); | ||
} | ||
if (errors.length > 0) { | ||
@@ -810,3 +828,5 @@ throw new ESLintInvalidOptionsError(errors); | ||
ignorePatterns, | ||
warnIgnored | ||
passOnNoPatterns, | ||
warnIgnored, | ||
ruleFilter | ||
}; | ||
@@ -813,0 +833,0 @@ } |
/** | ||
* @fileoverview Main API Class | ||
* @author Kai Cataldo | ||
* @author Toru Nagashima | ||
* @fileoverview Main class using flat config | ||
* @author Nicholas C. Zakas | ||
*/ | ||
@@ -13,7 +12,10 @@ | ||
// Note: Node.js 12 does not support fs/promises. | ||
const fs = require("fs").promises; | ||
const { existsSync } = require("fs"); | ||
const path = require("path"); | ||
const fs = require("fs"); | ||
const { promisify } = require("util"); | ||
const { CLIEngine, getCLIEngineInternalSlots } = require("../cli-engine/cli-engine"); | ||
const BuiltinRules = require("../rules"); | ||
const findUp = require("find-up"); | ||
const { version } = require("../../package.json"); | ||
const { Linter } = require("../linter"); | ||
const { getRuleFromConfig } = require("../config/flat-config-helpers"); | ||
const { | ||
@@ -23,7 +25,29 @@ Legacy: { | ||
getRuleSeverity | ||
} | ||
}, | ||
ModuleResolver, | ||
naming | ||
} | ||
} = require("@eslint/eslintrc"); | ||
const { version } = require("../../package.json"); | ||
const { | ||
findFiles, | ||
getCacheFile, | ||
isNonEmptyString, | ||
isArrayOfNonEmptyString, | ||
createIgnoreResult, | ||
isErrorMessage, | ||
processOptions | ||
} = require("./eslint-helpers"); | ||
const { pathToFileURL } = require("url"); | ||
const { FlatConfigArray } = require("../config/flat-config-array"); | ||
const LintResultCache = require("../cli-engine/lint-result-cache"); | ||
/* | ||
* This is necessary to allow overwriting writeFile for testing purposes. | ||
* We can just use fs/promises once we drop Node.js 12 support. | ||
*/ | ||
//------------------------------------------------------------------------------ | ||
@@ -33,19 +57,15 @@ // Typedefs | ||
/** @typedef {import("../cli-engine/cli-engine").LintReport} CLIEngineLintReport */ | ||
// For VSCode IntelliSense | ||
/** @typedef {import("../shared/types").ConfigData} ConfigData */ | ||
/** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ | ||
/** @typedef {import("../shared/types").ConfigData} ConfigData */ | ||
/** @typedef {import("../shared/types").LintMessage} LintMessage */ | ||
/** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */ | ||
/** @typedef {import("../shared/types").LintResult} LintResult */ | ||
/** @typedef {import("../shared/types").ParserOptions} ParserOptions */ | ||
/** @typedef {import("../shared/types").Plugin} Plugin */ | ||
/** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */ | ||
/** @typedef {import("../shared/types").RuleConf} RuleConf */ | ||
/** @typedef {import("../shared/types").Rule} Rule */ | ||
/** @typedef {import("../shared/types").LintResult} LintResult */ | ||
/** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */ | ||
/** @typedef {ReturnType<ConfigArray.extractConfig>} ExtractedConfig */ | ||
/** | ||
* The main formatter object. | ||
* @typedef LoadedFormatter | ||
* @property {(results: LintResult[], resultsMeta: ResultsMeta) => string | Promise<string>} format format function. | ||
*/ | ||
/** | ||
* The options with which to configure the ESLint instance. | ||
@@ -60,31 +80,17 @@ * @typedef {Object} ESLintOptions | ||
* @property {boolean} [errorOnUnmatchedPattern] If `false` then `ESLint#lintFiles()` doesn't throw even if no target files found. Defaults to `true`. | ||
* @property {string[]} [extensions] An array of file extensions to check. | ||
* @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean. | ||
* @property {string[]} [fixTypes] Array of rule types to apply fixes for. | ||
* @property {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. | ||
* @property {boolean} [ignore] False disables use of .eslintignore. | ||
* @property {string} [ignorePath] The ignore file to use instead of .eslintignore. | ||
* @property {boolean} [ignore] False disables all ignore patterns except for the default ones. | ||
* @property {string[]} [ignorePatterns] Ignore file patterns to use in addition to config ignores. These patterns are relative to `cwd`. | ||
* @property {ConfigData} [overrideConfig] Override config object, overrides all configs used with this instance | ||
* @property {string} [overrideConfigFile] The configuration file to use. | ||
* @property {Record<string,Plugin>|null} [plugins] Preloaded plugins. This is a map-like object, keys are plugin IDs and each value is implementation. | ||
* @property {"error" | "warn" | "off"} [reportUnusedDisableDirectives] the severity to report unused eslint-disable directives. | ||
* @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD. | ||
* @property {string[]} [rulePaths] An array of directories to load custom rules from. | ||
* @property {boolean} [useEslintrc] False disables looking for .eslintrc.* files. | ||
* @property {boolean|string} [overrideConfigFile] Searches for default config file when falsy; | ||
* doesn't do any config file lookup when `true`; considered to be a config filename | ||
* when a string. | ||
* @property {Record<string,Plugin>} [plugins] An array of plugin implementations. | ||
* @property {boolean} warnIgnored Show warnings when the file list includes ignored files | ||
* @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause | ||
* the linting operation to short circuit and not report any failures. | ||
*/ | ||
/** | ||
* A rules metadata object. | ||
* @typedef {Object} RulesMeta | ||
* @property {string} id The plugin ID. | ||
* @property {Object} definition The plugin definition. | ||
*/ | ||
/** | ||
* Private members for the `ESLint` instance. | ||
* @typedef {Object} ESLintPrivateMembers | ||
* @property {CLIEngine} cliEngine The wrapped CLIEngine instance. | ||
* @property {ESLintOptions} options The options used to instantiate the ESLint instance. | ||
*/ | ||
//------------------------------------------------------------------------------ | ||
@@ -94,233 +100,56 @@ // Helpers | ||
const writeFile = promisify(fs.writeFile); | ||
const FLAT_CONFIG_FILENAMES = [ | ||
"eslint.config.js", | ||
"eslint.config.mjs", | ||
"eslint.config.cjs" | ||
]; | ||
const debug = require("debug")("eslint:eslint"); | ||
const privateMembers = new WeakMap(); | ||
const importedConfigFileModificationTime = new Map(); | ||
const removedFormatters = new Set([ | ||
"checkstyle", | ||
"codeframe", | ||
"compact", | ||
"jslint-xml", | ||
"junit", | ||
"table", | ||
"tap", | ||
"unix", | ||
"visualstudio" | ||
]); | ||
/** | ||
* The map with which to store private class members. | ||
* @type {WeakMap<ESLint, ESLintPrivateMembers>} | ||
* 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 | ||
*/ | ||
const privateMembersMap = new WeakMap(); | ||
function calculateStatsPerFile(messages) { | ||
const stat = { | ||
errorCount: 0, | ||
fatalErrorCount: 0, | ||
warningCount: 0, | ||
fixableErrorCount: 0, | ||
fixableWarningCount: 0 | ||
}; | ||
/** | ||
* Check if a given value is a non-empty string or not. | ||
* @param {any} x The value to check. | ||
* @returns {boolean} `true` if `x` is a non-empty string. | ||
*/ | ||
function isNonEmptyString(x) { | ||
return typeof x === "string" && x.trim() !== ""; | ||
} | ||
for (let i = 0; i < messages.length; i++) { | ||
const message = messages[i]; | ||
/** | ||
* Check if a given value is an array of non-empty strings or not. | ||
* @param {any} x The value to check. | ||
* @returns {boolean} `true` if `x` is an array of non-empty strings. | ||
*/ | ||
function isArrayOfNonEmptyString(x) { | ||
return Array.isArray(x) && x.every(isNonEmptyString); | ||
} | ||
/** | ||
* 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", | ||
cwd = process.cwd(), | ||
errorOnUnmatchedPattern = true, | ||
extensions = null, // ← should be null by default because if it's an array then it suppresses RFC20 feature. | ||
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. | ||
globInputPaths = true, | ||
ignore = true, | ||
ignorePath = null, // ← should be null by default because if it's a string then it may throw ENOENT. | ||
overrideConfig = null, | ||
overrideConfigFile = null, | ||
plugins = {}, | ||
reportUnusedDisableDirectives = null, // ← should be null by default because if it's a string then it overrides the 'reportUnusedDisableDirectives' setting in config files. And we cannot use `overrideConfig.reportUnusedDisableDirectives` instead because we cannot configure the `error` severity with that. | ||
resolvePluginsRelativeTo = null, // ← should be null by default because if it's a string then it suppresses RFC47 feature. | ||
rulePaths = [], | ||
useEslintrc = 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. Please use the 'overrideConfig.env' option instead."); | ||
} | ||
if (unknownOptionKeys.includes("globals")) { | ||
errors.push("'globals' has been removed. Please use the 'overrideConfig.globals' option instead."); | ||
} | ||
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.parser' option instead."); | ||
} | ||
if (unknownOptionKeys.includes("parserOptions")) { | ||
errors.push("'parserOptions' has been removed. Please use the 'overrideConfig.parserOptions' option instead."); | ||
} | ||
if (unknownOptionKeys.includes("rules")) { | ||
errors.push("'rules' has been removed. Please use the 'overrideConfig.rules' 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 (!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 (!isArrayOfNonEmptyString(extensions) && extensions !== null) { | ||
errors.push("'extensions' must be an array of non-empty strings or null."); | ||
} | ||
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 (typeof globInputPaths !== "boolean") { | ||
errors.push("'globInputPaths' must be a boolean."); | ||
} | ||
if (typeof ignore !== "boolean") { | ||
errors.push("'ignore' must be a boolean."); | ||
} | ||
if (!isNonEmptyString(ignorePath) && ignorePath !== null) { | ||
errors.push("'ignorePath' must be a non-empty string or null."); | ||
} | ||
if (typeof overrideConfig !== "object") { | ||
errors.push("'overrideConfig' must be an object or null."); | ||
} | ||
if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null) { | ||
errors.push("'overrideConfigFile' must be a non-empty string or null."); | ||
} | ||
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 ( | ||
reportUnusedDisableDirectives !== "error" && | ||
reportUnusedDisableDirectives !== "warn" && | ||
reportUnusedDisableDirectives !== "off" && | ||
reportUnusedDisableDirectives !== null | ||
) { | ||
errors.push("'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null."); | ||
} | ||
if ( | ||
!isNonEmptyString(resolvePluginsRelativeTo) && | ||
resolvePluginsRelativeTo !== null | ||
) { | ||
errors.push("'resolvePluginsRelativeTo' must be a non-empty string or null."); | ||
} | ||
if (!isArrayOfNonEmptyString(rulePaths)) { | ||
errors.push("'rulePaths' must be an array of non-empty strings."); | ||
} | ||
if (typeof useEslintrc !== "boolean") { | ||
errors.push("'useEslintrc' must be a boolean."); | ||
} | ||
if (errors.length > 0) { | ||
throw new ESLintInvalidOptionsError(errors); | ||
} | ||
return { | ||
allowInlineConfig, | ||
baseConfig, | ||
cache, | ||
cacheLocation, | ||
cacheStrategy, | ||
configFile: overrideConfigFile, | ||
cwd: path.normalize(cwd), | ||
errorOnUnmatchedPattern, | ||
extensions, | ||
fix, | ||
fixTypes, | ||
globInputPaths, | ||
ignore, | ||
ignorePath, | ||
reportUnusedDisableDirectives, | ||
resolvePluginsRelativeTo, | ||
rulePaths, | ||
useEslintrc | ||
}; | ||
} | ||
/** | ||
* Check if a value has one or more properties and that value is not undefined. | ||
* @param {any} obj The value to check. | ||
* @returns {boolean} `true` if `obj` has one or more properties that that value is not undefined. | ||
*/ | ||
function hasDefinedProperty(obj) { | ||
if (typeof obj === "object" && obj !== null) { | ||
for (const key in obj) { | ||
if (typeof obj[key] !== "undefined") { | ||
return true; | ||
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 false; | ||
return stat; | ||
} | ||
@@ -340,2 +169,12 @@ | ||
/** | ||
* 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"); | ||
} | ||
/** @type {WeakMap<ExtractedConfig, DeprecatedRuleInfo[]>} */ | ||
@@ -346,38 +185,39 @@ const usedDeprecatedRulesCache = new WeakMap(); | ||
* Create used deprecated rule list. | ||
* @param {CLIEngine} cliEngine The CLIEngine instance. | ||
* @param {CLIEngine} eslint The CLIEngine instance. | ||
* @param {string} maybeFilePath The absolute path to a lint target file or `"<text>"`. | ||
* @returns {DeprecatedRuleInfo[]} The used deprecated rule list. | ||
*/ | ||
function getOrFindUsedDeprecatedRules(cliEngine, maybeFilePath) { | ||
function getOrFindUsedDeprecatedRules(eslint, maybeFilePath) { | ||
const { | ||
configArrayFactory, | ||
configs, | ||
options: { cwd } | ||
} = getCLIEngineInternalSlots(cliEngine); | ||
} = privateMembers.get(eslint); | ||
const filePath = path.isAbsolute(maybeFilePath) | ||
? maybeFilePath | ||
: path.join(cwd, "__placeholder__.js"); | ||
const configArray = configArrayFactory.getConfigArrayForFile(filePath); | ||
const config = configArray.extractConfig(filePath); | ||
: getPlaceholderPath(cwd); | ||
const config = configs.getConfig(filePath); | ||
// Most files use the same config, so cache it. | ||
if (!usedDeprecatedRulesCache.has(config)) { | ||
const pluginRules = configArray.pluginRules; | ||
if (config && !usedDeprecatedRulesCache.has(config)) { | ||
const retv = []; | ||
for (const [ruleId, ruleConf] of Object.entries(config.rules)) { | ||
if (getRuleSeverity(ruleConf) === 0) { | ||
continue; | ||
} | ||
const rule = pluginRules.get(ruleId) || BuiltinRules.get(ruleId); | ||
const meta = rule && rule.meta; | ||
if (config.rules) { | ||
for (const [ruleId, ruleConf] of Object.entries(config.rules)) { | ||
if (getRuleSeverity(ruleConf) === 0) { | ||
continue; | ||
} | ||
const rule = getRuleFromConfig(ruleId, config); | ||
const meta = rule && rule.meta; | ||
if (meta && meta.deprecated) { | ||
retv.push({ ruleId, replacedBy: meta.replacedBy || [] }); | ||
if (meta && meta.deprecated) { | ||
retv.push({ ruleId, replacedBy: meta.replacedBy || [] }); | ||
} | ||
} | ||
} | ||
usedDeprecatedRulesCache.set(config, Object.freeze(retv)); | ||
} | ||
return usedDeprecatedRulesCache.get(config); | ||
return config ? usedDeprecatedRulesCache.get(config) : Object.freeze([]); | ||
} | ||
@@ -388,7 +228,7 @@ | ||
* match the ESLint class's API. | ||
* @param {CLIEngine} cliEngine The CLIEngine instance. | ||
* @param {CLIEngine} eslint The CLIEngine instance. | ||
* @param {CLIEngineLintReport} report The CLIEngine linting report to process. | ||
* @returns {LintResult[]} The processed linting results. | ||
*/ | ||
function processCLIEngineLintReport(cliEngine, { results }) { | ||
function processLintReport(eslint, { results }) { | ||
const descriptor = { | ||
@@ -398,3 +238,3 @@ configurable: true, | ||
get() { | ||
return getOrFindUsedDeprecatedRules(cliEngine, this.filePath); | ||
return getOrFindUsedDeprecatedRules(eslint, this.filePath); | ||
} | ||
@@ -429,4 +269,308 @@ }; | ||
/** | ||
* Main API. | ||
* Searches from the current working directory up until finding the | ||
* given flat config filename. | ||
* @param {string} cwd The current working directory to search from. | ||
* @returns {Promise<string|undefined>} The filename if found or `undefined` if not. | ||
*/ | ||
function findFlatConfigFile(cwd) { | ||
return findUp( | ||
FLAT_CONFIG_FILENAMES, | ||
{ cwd } | ||
); | ||
} | ||
/** | ||
* Load the config array from the given filename. | ||
* @param {string} filePath The filename to load from. | ||
* @returns {Promise<any>} The config loaded from the config file. | ||
*/ | ||
async function loadFlatConfigFile(filePath) { | ||
debug(`Loading config from ${filePath}`); | ||
const fileURL = pathToFileURL(filePath); | ||
debug(`Config file URL is ${fileURL}`); | ||
const mtime = (await fs.stat(filePath)).mtime.getTime(); | ||
/* | ||
* Append a query with the config file's modification time (`mtime`) in order | ||
* to import the current version of the config file. Without the query, `import()` would | ||
* cache the config file module by the pathname only, and then always return | ||
* the same version (the one that was actual when the module was imported for the first time). | ||
* | ||
* This ensures that the config file module is loaded and executed again | ||
* if it has been changed since the last time it was imported. | ||
* If it hasn't been changed, `import()` will just return the cached version. | ||
* | ||
* Note that we should not overuse queries (e.g., by appending the current time | ||
* to always reload the config file module) as that could cause memory leaks | ||
* because entries are never removed from the import cache. | ||
*/ | ||
fileURL.searchParams.append("mtime", mtime); | ||
/* | ||
* With queries, we can bypass the import cache. However, when import-ing a CJS module, | ||
* Node.js uses the require infrastructure under the hood. That includes the require cache, | ||
* which caches the config file module by its file path (queries have no effect). | ||
* Therefore, we also need to clear the require cache before importing the config file module. | ||
* In order to get the same behavior with ESM and CJS config files, in particular - to reload | ||
* the config file only if it has been changed, we track file modification times and clear | ||
* the require cache only if the file has been changed. | ||
*/ | ||
if (importedConfigFileModificationTime.get(filePath) !== mtime) { | ||
delete require.cache[filePath]; | ||
} | ||
const config = (await import(fileURL)).default; | ||
importedConfigFileModificationTime.set(filePath, mtime); | ||
return config; | ||
} | ||
/** | ||
* Determines which config file to use. This is determined by seeing if an | ||
* override config file was passed, and if so, using it; otherwise, as long | ||
* as override config file is not explicitly set to `false`, it will search | ||
* upwards from the cwd for a file named `eslint.config.js`. | ||
* @param {import("./eslint").ESLintOptions} options The ESLint instance options. | ||
* @returns {{configFilePath:string|undefined,basePath:string,error:Error|null}} Location information for | ||
* the config file. | ||
*/ | ||
async function locateConfigFileToUse({ configFile, cwd }) { | ||
// determine where to load config file from | ||
let configFilePath; | ||
let basePath = cwd; | ||
let error = null; | ||
if (typeof configFile === "string") { | ||
debug(`Override config file path is ${configFile}`); | ||
configFilePath = path.resolve(cwd, configFile); | ||
} else if (configFile !== false) { | ||
debug("Searching for eslint.config.js"); | ||
configFilePath = await findFlatConfigFile(cwd); | ||
if (configFilePath) { | ||
basePath = path.resolve(path.dirname(configFilePath)); | ||
} else { | ||
error = new Error("Could not find config file."); | ||
} | ||
} | ||
return { | ||
configFilePath, | ||
basePath, | ||
error | ||
}; | ||
} | ||
/** | ||
* Calculates the config array for this run based on inputs. | ||
* @param {ESLint} eslint The instance to create the config array for. | ||
* @param {import("./eslint").ESLintOptions} options The ESLint instance options. | ||
* @returns {FlatConfigArray} The config array for `eslint``. | ||
*/ | ||
async function calculateConfigArray(eslint, { | ||
cwd, | ||
baseConfig, | ||
overrideConfig, | ||
configFile, | ||
ignore: shouldIgnore, | ||
ignorePatterns | ||
}) { | ||
// check for cached instance | ||
const slots = privateMembers.get(eslint); | ||
if (slots.configs) { | ||
return slots.configs; | ||
} | ||
const { configFilePath, basePath, error } = await locateConfigFileToUse({ configFile, cwd }); | ||
// config file is required to calculate config | ||
if (error) { | ||
throw error; | ||
} | ||
const configs = new FlatConfigArray(baseConfig || [], { basePath, shouldIgnore }); | ||
// load config file | ||
if (configFilePath) { | ||
const fileConfig = await loadFlatConfigFile(configFilePath); | ||
if (Array.isArray(fileConfig)) { | ||
configs.push(...fileConfig); | ||
} else { | ||
configs.push(fileConfig); | ||
} | ||
} | ||
// add in any configured defaults | ||
configs.push(...slots.defaultConfigs); | ||
// append command line ignore patterns | ||
if (ignorePatterns && ignorePatterns.length > 0) { | ||
let relativeIgnorePatterns; | ||
/* | ||
* If the config file basePath is different than the cwd, then | ||
* the ignore patterns won't work correctly. Here, we adjust the | ||
* ignore pattern to include the correct relative path. Patterns | ||
* passed as `ignorePatterns` are relative to the cwd, whereas | ||
* the config file basePath can be an ancestor of the cwd. | ||
*/ | ||
if (basePath === cwd) { | ||
relativeIgnorePatterns = ignorePatterns; | ||
} else { | ||
const relativeIgnorePath = path.relative(basePath, cwd); | ||
relativeIgnorePatterns = ignorePatterns.map(pattern => { | ||
const negated = pattern.startsWith("!"); | ||
const basePattern = negated ? pattern.slice(1) : pattern; | ||
return (negated ? "!" : "") + | ||
path.posix.join(relativeIgnorePath, basePattern); | ||
}); | ||
} | ||
/* | ||
* Ignore patterns are added to the end of the config array | ||
* so they can override default ignores. | ||
*/ | ||
configs.push({ | ||
ignores: relativeIgnorePatterns | ||
}); | ||
} | ||
if (overrideConfig) { | ||
if (Array.isArray(overrideConfig)) { | ||
configs.push(...overrideConfig); | ||
} else { | ||
configs.push(overrideConfig); | ||
} | ||
} | ||
await configs.normalize(); | ||
// cache the config array for this instance | ||
slots.configs = configs; | ||
return configs; | ||
} | ||
/** | ||
* Processes an 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 {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, | ||
linter | ||
}) { | ||
const filePath = providedFilePath || "<text>"; | ||
debug(`Lint ${filePath}`); | ||
/* | ||
* 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, | ||
/** | ||
* 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.isExplicitMatch(blockFilename); | ||
} | ||
} | ||
); | ||
// 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; | ||
} | ||
return result; | ||
} | ||
/** | ||
* Checks whether a message's rule type should be fixed. | ||
* @param {LintMessage} message The message to check. | ||
* @param {FlatConfig} 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 && getRuleFromConfig(message.ruleId, config); | ||
return Boolean(rule && rule.meta && fixTypes.has(rule.meta.type)); | ||
} | ||
/** | ||
* Creates an error to be thrown when an array of results passed to `getRulesMetaForResults` was not created by the current engine. | ||
* @returns {TypeError} An error object. | ||
*/ | ||
function createExtraneousResultsError() { | ||
return new TypeError("Results object was not created from this ESLint instance."); | ||
} | ||
//----------------------------------------------------------------------------- | ||
// Main API | ||
//----------------------------------------------------------------------------- | ||
/** | ||
* Primary Node.js API for ESLint. | ||
*/ | ||
class ESLint { | ||
@@ -439,31 +583,45 @@ | ||
constructor(options = {}) { | ||
const defaultConfigs = []; | ||
const processedOptions = processOptions(options); | ||
const cliEngine = new CLIEngine(processedOptions, { preloadedPlugins: options.plugins }); | ||
const { | ||
configArrayFactory, | ||
lastConfigArrays | ||
} = getCLIEngineInternalSlots(cliEngine); | ||
let updated = false; | ||
const linter = new Linter({ | ||
cwd: processedOptions.cwd, | ||
configType: "flat" | ||
}); | ||
/* | ||
* Address `overrideConfig` to set override config. | ||
* Operate the `configArrayFactory` internal slot directly because this | ||
* functionality doesn't exist as the public API of CLIEngine. | ||
const cacheFilePath = getCacheFile( | ||
processedOptions.cacheLocation, | ||
processedOptions.cwd | ||
); | ||
const lintResultCache = processedOptions.cache | ||
? new LintResultCache(cacheFilePath, processedOptions.cacheStrategy) | ||
: null; | ||
privateMembers.set(this, { | ||
options: processedOptions, | ||
linter, | ||
cacheFilePath, | ||
lintResultCache, | ||
defaultConfigs, | ||
configs: null | ||
}); | ||
/** | ||
* If additional plugins are passed in, add that to the default | ||
* configs for this instance. | ||
*/ | ||
if (hasDefinedProperty(options.overrideConfig)) { | ||
configArrayFactory.setOverrideConfig(options.overrideConfig); | ||
updated = true; | ||
} | ||
if (options.plugins) { | ||
// Update caches. | ||
if (updated) { | ||
configArrayFactory.clearCache(); | ||
lastConfigArrays[0] = configArrayFactory.getConfigArrayForFile(); | ||
const plugins = {}; | ||
for (const [pluginName, plugin] of Object.entries(options.plugins)) { | ||
plugins[naming.getShorthandName(pluginName, "eslint-plugin")] = plugin; | ||
} | ||
defaultConfigs.push({ | ||
plugins | ||
}); | ||
} | ||
// Initialize private properties. | ||
privateMembersMap.set(this, { | ||
cliEngine, | ||
options: processedOptions | ||
}); | ||
} | ||
@@ -500,3 +658,3 @@ | ||
}) | ||
.map(r => writeFile(r.filePath, r.output)) | ||
.map(r => fs.writeFile(r.filePath, r.output)) | ||
); | ||
@@ -511,3 +669,22 @@ } | ||
static getErrorResults(results) { | ||
return CLIEngine.getErrorResults(results); | ||
const filtered = []; | ||
results.forEach(result => { | ||
const filteredMessages = result.messages.filter(isErrorMessage); | ||
const filteredSuppressedMessages = result.suppressedMessages.filter(isErrorMessage); | ||
if (filteredMessages.length > 0) { | ||
filtered.push({ | ||
...result, | ||
messages: filteredMessages, | ||
suppressedMessages: filteredSuppressedMessages, | ||
errorCount: filteredMessages.length, | ||
warningCount: 0, | ||
fixableErrorCount: result.fixableErrorCount, | ||
fixableWarningCount: 0 | ||
}); | ||
} | ||
}); | ||
return filtered; | ||
} | ||
@@ -519,27 +696,57 @@ | ||
* @returns {Object} A mapping of ruleIds to rule meta objects. | ||
* @throws {TypeError} When the results object wasn't created from this ESLint instance. | ||
* @throws {TypeError} When a plugin or rule is missing. | ||
*/ | ||
getRulesMetaForResults(results) { | ||
const resultRuleIds = new Set(); | ||
// short-circuit simple case | ||
if (results.length === 0) { | ||
return {}; | ||
} | ||
// first gather all ruleIds from all results | ||
const resultRules = new Map(); | ||
const { | ||
configs, | ||
options: { cwd } | ||
} = privateMembers.get(this); | ||
for (const result of results) { | ||
for (const { ruleId } of result.messages) { | ||
resultRuleIds.add(ruleId); | ||
} | ||
for (const { ruleId } of result.suppressedMessages) { | ||
resultRuleIds.add(ruleId); | ||
} | ||
/* | ||
* We can only accurately return rules meta information for linting results if the | ||
* results were created by this instance. Otherwise, the necessary rules data is | ||
* not available. So if the config array doesn't already exist, just throw an error | ||
* to let the user know we can't do anything here. | ||
*/ | ||
if (!configs) { | ||
throw createExtraneousResultsError(); | ||
} | ||
// create a map of all rules in the results | ||
for (const result of results) { | ||
const { cliEngine } = privateMembersMap.get(this); | ||
const rules = cliEngine.getRules(); | ||
const resultRules = new Map(); | ||
/* | ||
* Normalize filename for <text>. | ||
*/ | ||
const filePath = result.filePath === "<text>" | ||
? getPlaceholderPath(cwd) : result.filePath; | ||
const allMessages = result.messages.concat(result.suppressedMessages); | ||
for (const [ruleId, rule] of rules) { | ||
if (resultRuleIds.has(ruleId)) { | ||
resultRules.set(ruleId, rule); | ||
for (const { ruleId } of allMessages) { | ||
if (!ruleId) { | ||
continue; | ||
} | ||
/* | ||
* All of the plugin and rule information is contained within the | ||
* calculated config for the given file. | ||
*/ | ||
const config = configs.getConfig(filePath); | ||
if (!config) { | ||
throw createExtraneousResultsError(); | ||
} | ||
const rule = getRuleFromConfig(ruleId, config); | ||
// ignore unknown rules | ||
if (rule) { | ||
resultRules.set(ruleId, rule); | ||
} | ||
} | ||
@@ -549,3 +756,2 @@ } | ||
return createRulesMeta(resultRules); | ||
} | ||
@@ -555,15 +761,193 @@ | ||
* Executes the current configuration on an array of file and directory names. | ||
* @param {string[]} patterns An array of file and directory names. | ||
* @param {string|string[]} patterns An array of file and directory names. | ||
* @returns {Promise<LintResult[]>} The results of linting the file patterns given. | ||
*/ | ||
async lintFiles(patterns) { | ||
if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) { | ||
throw new Error("'patterns' must be a non-empty string or an array of non-empty strings"); | ||
let normalizedPatterns = patterns; | ||
const { | ||
cacheFilePath, | ||
lintResultCache, | ||
linter, | ||
options: eslintOptions | ||
} = privateMembers.get(this); | ||
/* | ||
* Special cases: | ||
* 1. `patterns` is an empty string | ||
* 2. `patterns` is an empty array | ||
* | ||
* In both cases, we use the cwd as the directory to lint. | ||
*/ | ||
if (patterns === "" || Array.isArray(patterns) && patterns.length === 0) { | ||
/* | ||
* Special case: If `passOnNoPatterns` is true, then we just exit | ||
* without doing any work. | ||
*/ | ||
if (eslintOptions.passOnNoPatterns) { | ||
return []; | ||
} | ||
normalizedPatterns = ["."]; | ||
} else { | ||
if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) { | ||
throw new Error("'patterns' must be a non-empty string or an array of non-empty strings"); | ||
} | ||
if (typeof patterns === "string") { | ||
normalizedPatterns = [patterns]; | ||
} | ||
} | ||
const { cliEngine } = privateMembersMap.get(this); | ||
return processCLIEngineLintReport( | ||
cliEngine, | ||
cliEngine.executeOnFiles(patterns) | ||
debug(`Using file patterns: ${normalizedPatterns}`); | ||
const configs = await calculateConfigArray(this, eslintOptions); | ||
const { | ||
allowInlineConfig, | ||
cache, | ||
cwd, | ||
fix, | ||
fixTypes, | ||
ruleFilter, | ||
globInputPaths, | ||
errorOnUnmatchedPattern, | ||
warnIgnored | ||
} = eslintOptions; | ||
const startTime = Date.now(); | ||
const fixTypesSet = fixTypes ? new Set(fixTypes) : null; | ||
// Delete cache file; should this be done here? | ||
if (!cache && cacheFilePath) { | ||
debug(`Deleting cache file at ${cacheFilePath}`); | ||
try { | ||
await fs.unlink(cacheFilePath); | ||
} catch (error) { | ||
const errorCode = error && error.code; | ||
// Ignore errors when no such file exists or file system is read only (and cache file does not exist) | ||
if (errorCode !== "ENOENT" && !(errorCode === "EROFS" && !existsSync(cacheFilePath))) { | ||
throw error; | ||
} | ||
} | ||
} | ||
const filePaths = await findFiles({ | ||
patterns: normalizedPatterns, | ||
cwd, | ||
globInputPaths, | ||
configs, | ||
errorOnUnmatchedPattern | ||
}); | ||
debug(`${filePaths.length} files found in: ${Date.now() - startTime}ms`); | ||
/* | ||
* Because we need to process multiple files, including reading from disk, | ||
* it is most efficient to start by reading each file via promises so that | ||
* they can be done in parallel. Then, we can lint the returned text. This | ||
* ensures we are waiting the minimum amount of time in between lints. | ||
*/ | ||
const results = await Promise.all( | ||
filePaths.map(({ filePath, ignored }) => { | ||
/* | ||
* If a filename was entered that matches an ignore | ||
* pattern, then notify the user. | ||
*/ | ||
if (ignored) { | ||
if (warnIgnored) { | ||
return createIgnoreResult(filePath, cwd); | ||
} | ||
return void 0; | ||
} | ||
const config = configs.getConfig(filePath); | ||
/* | ||
* Sometimes a file found through a glob pattern will | ||
* be ignored. In this case, `config` will be undefined | ||
* and we just silently ignore the file. | ||
*/ | ||
if (!config) { | ||
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 | ||
let fixer = fix; | ||
if (fix && fixTypesSet) { | ||
// save original value of options.fix in case it's a function | ||
const originalFix = (typeof fix === "function") | ||
? fix : () => true; | ||
fixer = message => shouldMessageBeFixed(message, config, fixTypesSet) && originalFix(message); | ||
} | ||
return fs.readFile(filePath, "utf8") | ||
.then(text => { | ||
// do the linting | ||
const result = verifyText({ | ||
text, | ||
filePath, | ||
configs, | ||
cwd, | ||
fix: fixer, | ||
allowInlineConfig, | ||
ruleFilter, | ||
linter | ||
}); | ||
/* | ||
* Store the lint result in the LintResultCache. | ||
* NOTE: The LintResultCache will remove the file source and any | ||
* other properties that are difficult to serialize, and will | ||
* hydrate those properties back in on future lint runs. | ||
*/ | ||
if (lintResultCache) { | ||
lintResultCache.setCachedLintResults(filePath, config, result); | ||
} | ||
return result; | ||
}); | ||
}) | ||
); | ||
// Persist the cache to disk. | ||
if (lintResultCache) { | ||
lintResultCache.reconcile(); | ||
} | ||
const finalResults = results.filter(result => !!result); | ||
return processLintReport(this, { | ||
results: finalResults | ||
}); | ||
} | ||
@@ -580,11 +964,18 @@ | ||
async lintText(code, options = {}) { | ||
// Parameter validation | ||
if (typeof code !== "string") { | ||
throw new Error("'code' must be a string"); | ||
} | ||
if (typeof options !== "object") { | ||
throw new Error("'options' must be an object, null, or undefined"); | ||
} | ||
// Options validation | ||
const { | ||
filePath, | ||
warnIgnored = false, | ||
warnIgnored, | ||
...unknownOptions | ||
@@ -602,12 +993,53 @@ } = options || {}; | ||
} | ||
if (typeof warnIgnored !== "boolean") { | ||
if (typeof warnIgnored !== "boolean" && typeof warnIgnored !== "undefined") { | ||
throw new Error("'options.warnIgnored' must be a boolean or undefined"); | ||
} | ||
const { cliEngine } = privateMembersMap.get(this); | ||
// Now we can get down to linting | ||
return processCLIEngineLintReport( | ||
cliEngine, | ||
cliEngine.executeOnText(code, filePath, warnIgnored) | ||
); | ||
const { | ||
linter, | ||
options: eslintOptions | ||
} = privateMembers.get(this); | ||
const configs = await calculateConfigArray(this, eslintOptions); | ||
const { | ||
allowInlineConfig, | ||
cwd, | ||
fix, | ||
warnIgnored: constructorWarnIgnored, | ||
ruleFilter | ||
} = eslintOptions; | ||
const results = []; | ||
const startTime = Date.now(); | ||
const resolvedFilename = path.resolve(cwd, filePath || "__placeholder__.js"); | ||
// Clear the last used config arrays. | ||
if (resolvedFilename && await this.isPathIgnored(resolvedFilename)) { | ||
const shouldWarnIgnored = typeof warnIgnored === "boolean" ? warnIgnored : constructorWarnIgnored; | ||
if (shouldWarnIgnored) { | ||
results.push(createIgnoreResult(resolvedFilename, cwd)); | ||
} | ||
} else { | ||
// Do lint. | ||
results.push(verifyText({ | ||
text: code, | ||
filePath: resolvedFilename.endsWith("__placeholder__.js") ? "<text>" : resolvedFilename, | ||
configs, | ||
cwd, | ||
fix, | ||
allowInlineConfig, | ||
ruleFilter, | ||
linter | ||
})); | ||
} | ||
debug(`Linting complete in: ${Date.now() - startTime}ms`); | ||
return processLintReport(this, { | ||
results | ||
}); | ||
} | ||
@@ -626,3 +1058,3 @@ | ||
* - A file path ... Load the file. | ||
* @returns {Promise<LoadedFormatter>} A promise resolving to the formatter object. | ||
* @returns {Promise<Formatter>} A promise resolving to the formatter object. | ||
* This promise will be rejected if the given formatter was not found or not | ||
@@ -636,9 +1068,49 @@ * a function. | ||
const { cliEngine, options } = privateMembersMap.get(this); | ||
const formatter = cliEngine.getFormatter(name); | ||
// replace \ with / for Windows compatibility | ||
const normalizedFormatName = name.replace(/\\/gu, "/"); | ||
const namespace = naming.getNamespaceFromTerm(normalizedFormatName); | ||
// grab our options | ||
const { cwd } = privateMembers.get(this).options; | ||
let formatterPath; | ||
// if there's a slash, then it's a file (TODO: this check seems dubious for scoped npm packages) | ||
if (!namespace && normalizedFormatName.includes("/")) { | ||
formatterPath = path.resolve(cwd, normalizedFormatName); | ||
} else { | ||
try { | ||
const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter"); | ||
// TODO: This is pretty dirty...would be nice to clean up at some point. | ||
formatterPath = ModuleResolver.resolve(npmFormat, getPlaceholderPath(cwd)); | ||
} catch { | ||
formatterPath = path.resolve(__dirname, "../", "cli-engine", "formatters", `${normalizedFormatName}.js`); | ||
} | ||
} | ||
let formatter; | ||
try { | ||
formatter = (await import(pathToFileURL(formatterPath))).default; | ||
} catch (ex) { | ||
// check for formatters that have been removed | ||
if (removedFormatters.has(name)) { | ||
ex.message = `The ${name} formatter is no longer part of core ESLint. Install it manually with \`npm install -D eslint-formatter-${name}\``; | ||
} else { | ||
ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`; | ||
} | ||
throw ex; | ||
} | ||
if (typeof formatter !== "function") { | ||
throw new Error(`Formatter must be a function, but got a ${typeof formatter}.`); | ||
throw new TypeError(`Formatter must be a function, but got a ${typeof formatter}.`); | ||
} | ||
const eslint = this; | ||
return { | ||
@@ -648,5 +1120,5 @@ | ||
* The main formatter method. | ||
* @param {LintResult[]} results The lint results to format. | ||
* @param {LintResults[]} results The lint results to format. | ||
* @param {ResultsMeta} resultsMeta Warning count and max threshold. | ||
* @returns {string | Promise<string>} The formatted lint results. | ||
* @returns {string} The formatted lint results. | ||
*/ | ||
@@ -660,8 +1132,6 @@ format(results, resultsMeta) { | ||
...resultsMeta, | ||
get cwd() { | ||
return options.cwd; | ||
}, | ||
cwd, | ||
get rulesMeta() { | ||
if (!rulesMeta) { | ||
rulesMeta = createRulesMeta(cliEngine.getRules()); | ||
rulesMeta = eslint.getRulesMetaForResults(results); | ||
} | ||
@@ -681,3 +1151,4 @@ | ||
* @param {string} filePath The path of the file to retrieve a config object for. | ||
* @returns {Promise<ConfigData>} A configuration object for the file. | ||
* @returns {Promise<ConfigData|undefined>} A configuration object for the file | ||
* or `undefined` if there is no configuration data for the object. | ||
*/ | ||
@@ -688,8 +1159,23 @@ async calculateConfigForFile(filePath) { | ||
} | ||
const { cliEngine } = privateMembersMap.get(this); | ||
const options = privateMembers.get(this).options; | ||
const absolutePath = path.resolve(options.cwd, filePath); | ||
const configs = await calculateConfigArray(this, options); | ||
return cliEngine.getConfigForFile(filePath); | ||
return configs.getConfig(absolutePath); | ||
} | ||
/** | ||
* Finds the config file being used by this instance based on the options | ||
* passed to the constructor. | ||
* @returns {string|undefined} The path to the config file being used or | ||
* `undefined` if no config file is being used. | ||
*/ | ||
async findConfigFile() { | ||
const options = privateMembers.get(this).options; | ||
const { configFilePath } = await locateConfigFileToUse(options); | ||
return configFilePath; | ||
} | ||
/** | ||
* Checks if a given path is ignored by ESLint. | ||
@@ -700,11 +1186,16 @@ * @param {string} filePath The path of the file to check. | ||
async isPathIgnored(filePath) { | ||
if (!isNonEmptyString(filePath)) { | ||
throw new Error("'filePath' must be a non-empty string"); | ||
} | ||
const { cliEngine } = privateMembersMap.get(this); | ||
const config = await this.calculateConfigForFile(filePath); | ||
return cliEngine.isPathIgnored(filePath); | ||
return config === void 0; | ||
} | ||
} | ||
/** | ||
* Returns whether flat config should be used. | ||
* @returns {Promise<boolean>} Whether flat config should be used. | ||
*/ | ||
async function shouldUseFlatConfig() { | ||
return (process.env.ESLINT_USE_FLAT_CONFIG !== "false"); | ||
} | ||
//------------------------------------------------------------------------------ | ||
@@ -716,11 +1207,3 @@ // Public Interface | ||
ESLint, | ||
/** | ||
* Get the private class members of a given ESLint instance for tests. | ||
* @param {ESLint} instance The ESLint instance to get. | ||
* @returns {ESLintPrivateMembers} The instance's private class members. | ||
*/ | ||
getESLintPrivateMembers(instance) { | ||
return privateMembersMap.get(instance); | ||
} | ||
shouldUseFlatConfig | ||
}; |
"use strict"; | ||
const { ESLint } = require("./eslint"); | ||
const { FlatESLint } = require("./flat-eslint"); | ||
const { LegacyESLint } = require("./legacy-eslint"); | ||
module.exports = { | ||
ESLint, | ||
FlatESLint | ||
LegacyESLint | ||
}; |
@@ -19,2 +19,7 @@ /** | ||
const escapeRegExp = require("escape-string-regexp"); | ||
const { | ||
Legacy: { | ||
ConfigOps | ||
} | ||
} = require("@eslint/eslintrc/universal"); | ||
@@ -349,7 +354,7 @@ /** | ||
const unusedDisableDirectivesToReport = options.directives | ||
.filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive)); | ||
.filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive) && !options.rulesToIgnore.has(directive.ruleId)); | ||
const unusedEnableDirectivesToReport = new Set( | ||
options.directives.filter(directive => directive.unprocessedDirective.type === "enable") | ||
options.directives.filter(directive => directive.unprocessedDirective.type === "enable" && !options.rulesToIgnore.has(directive.ruleId)) | ||
); | ||
@@ -415,2 +420,4 @@ | ||
* @param {"off" | "warn" | "error"} options.reportUnusedDisableDirectives If `"warn"` or `"error"`, adds additional problems for unused directives | ||
* @param {Object} options.configuredRules The rules configuration. | ||
* @param {Function} options.ruleFilter A predicate function to filter which rules should be executed. | ||
* @param {boolean} options.disableFixes If true, it doesn't make `fix` properties. | ||
@@ -420,3 +427,3 @@ * @returns {{ruleId: (string|null), line: number, column: number, suppressions?: {kind: string, justification: string}}[]} | ||
*/ | ||
module.exports = ({ directives, disableFixes, problems, reportUnusedDisableDirectives = "off" }) => { | ||
module.exports = ({ directives, disableFixes, problems, configuredRules, ruleFilter, reportUnusedDisableDirectives = "off" }) => { | ||
const blockDirectives = directives | ||
@@ -450,2 +457,21 @@ .filter(directive => directive.type === "disable" || directive.type === "enable") | ||
// This determines a list of rules that are not being run by the given ruleFilter, if present. | ||
const rulesToIgnore = configuredRules && ruleFilter | ||
? new Set(Object.keys(configuredRules).filter(ruleId => { | ||
const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]); | ||
// Ignore for disabled rules. | ||
if (severity === 0) { | ||
return false; | ||
} | ||
return !ruleFilter({ severity, ruleId }); | ||
})) | ||
: new Set(); | ||
// If no ruleId is supplied that means this directive is applied to all rules, so we can't determine if it's unused if any rules are filtered out. | ||
if (rulesToIgnore.size > 0) { | ||
rulesToIgnore.add(null); | ||
} | ||
const blockDirectivesResult = applyDirectives({ | ||
@@ -455,3 +481,4 @@ problems, | ||
disableFixes, | ||
reportUnusedDisableDirectives | ||
reportUnusedDisableDirectives, | ||
rulesToIgnore | ||
}); | ||
@@ -462,3 +489,4 @@ const lineDirectivesResult = applyDirectives({ | ||
disableFixes, | ||
reportUnusedDisableDirectives | ||
reportUnusedDisableDirectives, | ||
rulesToIgnore | ||
}); | ||
@@ -465,0 +493,0 @@ |
@@ -43,3 +43,3 @@ /** | ||
* Parses a list of "name:string_value" or/and "name" options divided by comma or | ||
* whitespace. Used for "global" and "exported" comments. | ||
* whitespace. Used for "global" comments. | ||
* @param {string} string The string to parse. | ||
@@ -46,0 +46,0 @@ * @param {Comment} comment The comment node which has the string. |
@@ -16,14 +16,6 @@ /** | ||
//------------------------------------------------------------------------------ | ||
// Helpers | ||
// Typedefs | ||
//------------------------------------------------------------------------------ | ||
/** | ||
* Normalizes a rule module to the new-style API | ||
* @param {(Function|{create: Function})} rule A rule object, which can either be a function | ||
* ("old-style") or an object with a `create` method ("new-style") | ||
* @returns {{create: Function}} A new-style rule. | ||
*/ | ||
function normalizeRule(rule) { | ||
return typeof rule === "function" ? Object.assign({ create: rule }, rule) : rule; | ||
} | ||
/** @typedef {import("../shared/types").Rule} Rule */ | ||
@@ -45,7 +37,7 @@ //------------------------------------------------------------------------------ | ||
* @param {string} ruleId Rule id (file name). | ||
* @param {Function} ruleModule Rule handler. | ||
* @param {Rule} rule Rule object. | ||
* @returns {void} | ||
*/ | ||
define(ruleId, ruleModule) { | ||
this._rules[ruleId] = normalizeRule(ruleModule); | ||
define(ruleId, rule) { | ||
this._rules[ruleId] = rule; | ||
} | ||
@@ -56,4 +48,3 @@ | ||
* @param {string} ruleId Rule id (file name). | ||
* @returns {{create: Function, schema: JsonSchema[]}} | ||
* A rule. This is normalized to always have the new-style shape with a `create` method. | ||
* @returns {Rule} Rule object. | ||
*/ | ||
@@ -60,0 +51,0 @@ get(ruleId) { |
@@ -60,2 +60,4 @@ /** | ||
* @property {boolean} warnIgnored Show warnings when the file list includes ignored files | ||
* @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause | ||
* the linting operation to short circuit and not report any failures. | ||
* @property {string[]} _ Positional filenames or patterns | ||
@@ -172,3 +174,3 @@ */ | ||
description: usingFlatConfig | ||
? "Use this configuration instead of eslint.config.js" | ||
? "Use this configuration instead of eslint.config.js, eslint.config.mjs, or eslint.config.cjs" | ||
: "Use this configuration, overriding .eslintrc.* config options if present" | ||
@@ -376,2 +378,8 @@ }, | ||
{ | ||
option: "pass-on-no-patterns", | ||
type: "Boolean", | ||
default: false, | ||
description: "Exit with exit code 0 in case no file patterns are passed" | ||
}, | ||
{ | ||
option: "debug", | ||
@@ -378,0 +386,0 @@ type: "Boolean", |
/** | ||
* @fileoverview Mocha test wrapper | ||
* @fileoverview Mocha/Jest test wrapper | ||
* @author Ilya Volodin | ||
@@ -9,32 +9,2 @@ */ | ||
/* | ||
* This is a wrapper around mocha to allow for DRY unittests for eslint | ||
* Format: | ||
* RuleTester.run("{ruleName}", { | ||
* valid: [ | ||
* "{code}", | ||
* { code: "{code}", options: {options}, globals: {globals}, parser: "{parser}", settings: {settings} } | ||
* ], | ||
* invalid: [ | ||
* { code: "{code}", errors: {numErrors} }, | ||
* { code: "{code}", errors: ["{errorMessage}"] }, | ||
* { code: "{code}", options: {options}, globals: {globals}, parser: "{parser}", settings: {settings}, errors: [{ message: "{errorMessage}", type: "{errorNodeType}"}] } | ||
* ] | ||
* }); | ||
* | ||
* Variables: | ||
* {code} - String that represents the code to be tested | ||
* {options} - Arguments that are passed to the configurable rules. | ||
* {globals} - An object representing a list of variables that are | ||
* registered as globals | ||
* {parser} - String representing the parser to use | ||
* {settings} - An object representing global settings for all rules | ||
* {numErrors} - If failing case doesn't need to check error message, | ||
* this integer will specify how many errors should be | ||
* received | ||
* {errorMessage} - Message that is returned by the rule on failure | ||
* {errorNodeType} - AST node type that is returned by they rule as | ||
* a cause of the failure. | ||
*/ | ||
//------------------------------------------------------------------------------ | ||
@@ -46,17 +16,17 @@ // Requirements | ||
assert = require("assert"), | ||
path = require("path"), | ||
util = require("util"), | ||
merge = require("lodash.merge"), | ||
equal = require("fast-deep-equal"), | ||
Traverser = require("../../lib/shared/traverser"), | ||
{ getRuleOptionsSchema, validate } = require("../shared/config-validator"), | ||
Traverser = require("../shared/traverser"), | ||
{ getRuleOptionsSchema } = require("../config/flat-config-helpers"), | ||
{ Linter, SourceCodeFixer, interpolate } = require("../linter"), | ||
CodePath = require("../linter/code-path-analysis/code-path"); | ||
const { FlatConfigArray } = require("../config/flat-config-array"); | ||
const { defaultConfig } = require("../config/default-config"); | ||
const ajv = require("../shared/ajv")({ strictDefaults: true }); | ||
const espreePath = require.resolve("espree"); | ||
const parserSymbol = Symbol.for("eslint.RuleTester.parser"); | ||
const { SourceCode } = require("../source-code"); | ||
const { ConfigArraySymbol } = require("@humanwhocodes/config-array"); | ||
@@ -68,2 +38,3 @@ //------------------------------------------------------------------------------ | ||
/** @typedef {import("../shared/types").Parser} Parser */ | ||
/** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */ | ||
/** @typedef {import("../shared/types").Rule} Rule */ | ||
@@ -78,8 +49,5 @@ | ||
* @property {any[]} [options] Options for the test case. | ||
* @property {LanguageOptions} [languageOptions] The language options to use in the test case. | ||
* @property {{ [name: string]: any }} [settings] Settings for the test case. | ||
* @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames. | ||
* @property {string} [parser] The absolute path for the parser. | ||
* @property {{ [name: string]: any }} [parserOptions] Options for the parser. | ||
* @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables. | ||
* @property {{ [name: string]: boolean }} [env] Environments for the test case. | ||
* @property {boolean} [only] Run only this test case or the subset of test cases with this property. | ||
@@ -98,6 +66,3 @@ */ | ||
* @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames. | ||
* @property {string} [parser] The absolute path for the parser. | ||
* @property {{ [name: string]: any }} [parserOptions] Options for the parser. | ||
* @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables. | ||
* @property {{ [name: string]: boolean }} [env] Environments for the test case. | ||
* @property {LanguageOptions} [languageOptions] The language options to use in the test case. | ||
* @property {boolean} [only] Run only this test case or the subset of test cases with this property. | ||
@@ -128,5 +93,10 @@ */ | ||
const testerDefaultConfig = { rules: {} }; | ||
let defaultConfig = { rules: {} }; | ||
/* | ||
* RuleTester uses this config as its default. This can be overwritten via | ||
* setDefaultConfig(). | ||
*/ | ||
let sharedDefaultConfig = { rules: {} }; | ||
/* | ||
* List every parameters possible on a test case that are not related to eslint | ||
@@ -178,33 +148,7 @@ * configuration | ||
/** @type {Map<string,WeakSet>} */ | ||
const forbiddenMethodCalls = new Map(forbiddenMethods.map(methodName => ([methodName, new WeakSet()]))); | ||
const hasOwnProperty = Function.call.bind(Object.hasOwnProperty); | ||
const DEPRECATED_SOURCECODE_PASSTHROUGHS = { | ||
getSource: "getText", | ||
getSourceLines: "getLines", | ||
getAllComments: "getAllComments", | ||
getNodeByRangeIndex: "getNodeByRangeIndex", | ||
// getComments: "getComments", -- already handled by a separate error | ||
getCommentsBefore: "getCommentsBefore", | ||
getCommentsAfter: "getCommentsAfter", | ||
getCommentsInside: "getCommentsInside", | ||
getJSDocComment: "getJSDocComment", | ||
getFirstToken: "getFirstToken", | ||
getFirstTokens: "getFirstTokens", | ||
getLastToken: "getLastToken", | ||
getLastTokens: "getLastTokens", | ||
getTokenAfter: "getTokenAfter", | ||
getTokenBefore: "getTokenBefore", | ||
getTokenByRangeStart: "getTokenByRangeStart", | ||
getTokens: "getTokens", | ||
getTokensAfter: "getTokensAfter", | ||
getTokensBefore: "getTokensBefore", | ||
getTokensBetween: "getTokensBetween", | ||
getScope: "getScope", | ||
getAncestors: "getAncestors", | ||
getDeclaredVariables: "getDeclaredVariables", | ||
markVariableAsUsed: "markVariableAsUsed" | ||
}; | ||
/** | ||
@@ -341,72 +285,2 @@ * Clones a given value deeply. | ||
/** | ||
* Function to replace `SourceCode.prototype.getComments`. | ||
* @returns {void} | ||
* @throws {Error} Deprecation message. | ||
*/ | ||
function getCommentsDeprecation() { | ||
throw new Error( | ||
"`SourceCode#getComments()` is deprecated and will be removed in a future major version. Use `getCommentsBefore()`, `getCommentsAfter()`, and `getCommentsInside()` instead." | ||
); | ||
} | ||
/** | ||
* Function to replace forbidden `SourceCode` methods. | ||
* @param {string} methodName The name of the method to forbid. | ||
* @returns {Function} The function that throws the error. | ||
*/ | ||
function throwForbiddenMethodError(methodName) { | ||
return () => { | ||
throw new Error( | ||
`\`SourceCode#${methodName}()\` cannot be called inside a rule.` | ||
); | ||
}; | ||
} | ||
/** | ||
* Emit a deprecation warning if function-style format is being used. | ||
* @param {string} ruleName Name of the rule. | ||
* @returns {void} | ||
*/ | ||
function emitLegacyRuleAPIWarning(ruleName) { | ||
if (!emitLegacyRuleAPIWarning[`warned-${ruleName}`]) { | ||
emitLegacyRuleAPIWarning[`warned-${ruleName}`] = true; | ||
process.emitWarning( | ||
`"${ruleName}" rule is using the deprecated function-style format and will stop working in ESLint v9. Please use object-style format: https://eslint.org/docs/latest/extend/custom-rules`, | ||
"DeprecationWarning" | ||
); | ||
} | ||
} | ||
/** | ||
* Emit a deprecation warning if rule has options but is missing the "meta.schema" property | ||
* @param {string} ruleName Name of the rule. | ||
* @returns {void} | ||
*/ | ||
function emitMissingSchemaWarning(ruleName) { | ||
if (!emitMissingSchemaWarning[`warned-${ruleName}`]) { | ||
emitMissingSchemaWarning[`warned-${ruleName}`] = true; | ||
process.emitWarning( | ||
`"${ruleName}" rule has options but is missing the "meta.schema" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas`, | ||
"DeprecationWarning" | ||
); | ||
} | ||
} | ||
/** | ||
* Emit a deprecation warning if a rule uses a deprecated `context` method. | ||
* @param {string} ruleName Name of the rule. | ||
* @param {string} methodName The name of the method on `context` that was used. | ||
* @returns {void} | ||
*/ | ||
function emitDeprecatedContextMethodWarning(ruleName, methodName) { | ||
if (!emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`]) { | ||
emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`] = true; | ||
process.emitWarning( | ||
`"${ruleName}" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]}()\` instead.`, | ||
"DeprecationWarning" | ||
); | ||
} | ||
} | ||
/** | ||
* Emit a deprecation warning if rule uses CodePath#currentSegments. | ||
@@ -427,17 +301,37 @@ * @param {string} ruleName Name of the rule. | ||
/** | ||
* Emit a deprecation warning if `context.parserServices` is used. | ||
* @param {string} ruleName Name of the rule. | ||
* @returns {void} | ||
* Function to replace forbidden `SourceCode` methods. Allows just one call per method. | ||
* @param {string} methodName The name of the method to forbid. | ||
* @param {Function} prototype The prototype with the original method to call. | ||
* @returns {Function} The function that throws the error. | ||
*/ | ||
function emitParserServicesWarning(ruleName) { | ||
if (!emitParserServicesWarning[`warned-${ruleName}`]) { | ||
emitParserServicesWarning[`warned-${ruleName}`] = true; | ||
process.emitWarning( | ||
`"${ruleName}" rule is using \`context.parserServices\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.parserServices\` instead.`, | ||
"DeprecationWarning" | ||
function throwForbiddenMethodError(methodName, prototype) { | ||
const original = prototype[methodName]; | ||
return function(...args) { | ||
const called = forbiddenMethodCalls.get(methodName); | ||
/* eslint-disable no-invalid-this -- needed to operate as a method. */ | ||
if (!called.has(this)) { | ||
called.add(this); | ||
return original.apply(this, args); | ||
} | ||
/* eslint-enable no-invalid-this -- not needed past this point */ | ||
throw new Error( | ||
`\`SourceCode#${methodName}()\` cannot be called inside a rule.` | ||
); | ||
} | ||
}; | ||
} | ||
const metaSchemaDescription = ` | ||
\t- If the rule has options, set \`meta.schema\` to an array or non-empty object to enable options validation. | ||
\t- If the rule doesn't have options, omit \`meta.schema\` to enforce that no options can be passed to the rule. | ||
\t- You can also set \`meta.schema\` to \`false\` to opt-out of options validation (not recommended). | ||
\thttps://eslint.org/docs/latest/extend/custom-rules#options-schemas | ||
`; | ||
//------------------------------------------------------------------------------ | ||
@@ -491,3 +385,3 @@ // Public Interface | ||
*/ | ||
constructor(testerConfig) { | ||
constructor(testerConfig = {}) { | ||
@@ -499,15 +393,9 @@ /** | ||
*/ | ||
this.testerConfig = merge( | ||
{}, | ||
defaultConfig, | ||
this.testerConfig = [ | ||
sharedDefaultConfig, | ||
testerConfig, | ||
{ rules: { "rule-tester/validate-ast": "error" } } | ||
); | ||
]; | ||
/** | ||
* Rule definitions to define before tests. | ||
* @type {Object} | ||
*/ | ||
this.rules = {}; | ||
this.linter = new Linter(); | ||
this.linter = new Linter({ configType: "flat" }); | ||
} | ||
@@ -525,6 +413,6 @@ | ||
} | ||
defaultConfig = config; | ||
sharedDefaultConfig = config; | ||
// Make sure the rules object exists since it is assumed to exist later | ||
defaultConfig.rules = defaultConfig.rules || {}; | ||
sharedDefaultConfig.rules = sharedDefaultConfig.rules || {}; | ||
} | ||
@@ -537,3 +425,3 @@ | ||
static getDefaultConfig() { | ||
return defaultConfig; | ||
return sharedDefaultConfig; | ||
} | ||
@@ -547,3 +435,7 @@ | ||
static resetDefaultConfig() { | ||
defaultConfig = merge({}, testerDefaultConfig); | ||
sharedDefaultConfig = { | ||
rules: { | ||
...testerDefaultConfig.rules | ||
} | ||
}; | ||
} | ||
@@ -619,14 +511,2 @@ | ||
/** | ||
* Define a rule for one particular run of tests. | ||
* @param {string} name The name of the rule to define. | ||
* @param {Function | Rule} rule The rule definition. | ||
* @returns {void} | ||
*/ | ||
defineRule(name, rule) { | ||
if (typeof rule === "function") { | ||
emitLegacyRuleAPIWarning(name); | ||
} | ||
this.rules[name] = rule; | ||
} | ||
@@ -636,3 +516,3 @@ /** | ||
* @param {string} ruleName The name of the rule to run. | ||
* @param {Function | Rule} rule The rule to test. | ||
* @param {Rule} rule The rule to test. | ||
* @param {{ | ||
@@ -642,4 +522,4 @@ * valid: (ValidTestCase | string)[], | ||
* }} test The collection of tests to run. | ||
* @throws {TypeError|Error} If non-object `test`, or if a required | ||
* scenario of the given type is missing. | ||
* @throws {TypeError|Error} If `rule` is not an object with a `create` method, | ||
* or if non-object `test`, or if a required scenario of the given type is missing. | ||
* @returns {void} | ||
@@ -652,4 +532,9 @@ */ | ||
scenarioErrors = [], | ||
linter = this.linter; | ||
linter = this.linter, | ||
ruleId = `rule-to-test/${ruleName}`; | ||
if (!rule || typeof rule !== "object" || typeof rule.create !== "function") { | ||
throw new TypeError("Rule must be an object with a `create` method"); | ||
} | ||
if (!test || typeof test !== "object") { | ||
@@ -671,51 +556,52 @@ throw new TypeError(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`); | ||
if (typeof rule === "function") { | ||
emitLegacyRuleAPIWarning(ruleName); | ||
} | ||
const baseConfig = [ | ||
{ files: ["**"] }, // Make sure the default config matches for all files | ||
{ | ||
plugins: { | ||
linter.defineRule(ruleName, Object.assign({}, rule, { | ||
// copy root plugin over | ||
"@": { | ||
// Create a wrapper rule that freezes the `context` properties. | ||
create(context) { | ||
freezeDeeply(context.options); | ||
freezeDeeply(context.settings); | ||
freezeDeeply(context.parserOptions); | ||
/* | ||
* Parsers are wrapped to detect more errors, so this needs | ||
* to be a new object for each call to run(), otherwise the | ||
* parsers will be wrapped multiple times. | ||
*/ | ||
parsers: { | ||
...defaultConfig[0].plugins["@"].parsers | ||
}, | ||
// wrap all deprecated methods | ||
const newContext = Object.create( | ||
context, | ||
Object.fromEntries(Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).map(methodName => [ | ||
methodName, | ||
{ | ||
value(...args) { | ||
/* | ||
* The rules key on the default plugin is a proxy to lazy-load | ||
* just the rules that are needed. So, don't create a new object | ||
* here, just use the default one to keep that performance | ||
* enhancement. | ||
*/ | ||
rules: defaultConfig[0].plugins["@"].rules | ||
}, | ||
"rule-to-test": { | ||
rules: { | ||
[ruleName]: Object.assign({}, rule, { | ||
// emit deprecation warning | ||
emitDeprecatedContextMethodWarning(ruleName, methodName); | ||
// Create a wrapper rule that freezes the `context` properties. | ||
create(context) { | ||
freezeDeeply(context.options); | ||
freezeDeeply(context.settings); | ||
freezeDeeply(context.parserOptions); | ||
// call the original method | ||
return context[methodName].call(this, ...args); | ||
}, | ||
enumerable: true | ||
// freezeDeeply(context.languageOptions); | ||
return rule.create(context); | ||
} | ||
}) | ||
} | ||
])) | ||
); | ||
// emit warning about context.parserServices | ||
const parserServices = context.parserServices; | ||
Object.defineProperty(newContext, "parserServices", { | ||
get() { | ||
emitParserServicesWarning(ruleName); | ||
return parserServices; | ||
} | ||
}); | ||
}, | ||
languageOptions: { | ||
...defaultConfig[0].languageOptions | ||
} | ||
}, | ||
...defaultConfig.slice(1) | ||
]; | ||
Object.freeze(newContext); | ||
return (typeof rule === "function" ? rule : rule.create)(newContext); | ||
} | ||
})); | ||
linter.defineRules(this.rules); | ||
/** | ||
@@ -729,5 +615,23 @@ * Run the rule for the given item | ||
function runRuleForItem(item) { | ||
let config = merge({}, testerConfig), | ||
code, filename, output, beforeAST, afterAST; | ||
const configs = new FlatConfigArray(testerConfig, { baseConfig }); | ||
/* | ||
* Modify the returned config so that the parser is wrapped to catch | ||
* access of the start/end properties. This method is called just | ||
* once per code snippet being tested, so each test case gets a clean | ||
* parser. | ||
*/ | ||
configs[ConfigArraySymbol.finalizeConfig] = function(...args) { | ||
// can't do super here :( | ||
const proto = Object.getPrototypeOf(this); | ||
const calculatedConfig = proto[ConfigArraySymbol.finalizeConfig].apply(this, args); | ||
// wrap the parser to catch start/end property access | ||
calculatedConfig.languageOptions.parser = wrapParser(calculatedConfig.languageOptions.parser); | ||
return calculatedConfig; | ||
}; | ||
let code, filename, output, beforeAST, afterAST; | ||
if (typeof item === "string") { | ||
@@ -748,2 +652,13 @@ code = item; | ||
// wrap any parsers | ||
if (itemConfig.languageOptions && itemConfig.languageOptions.parser) { | ||
const parser = itemConfig.languageOptions.parser; | ||
if (parser && typeof parser !== "object") { | ||
throw new Error("Parser must be an object with a parse() or parseForESLint() method."); | ||
} | ||
} | ||
/* | ||
@@ -753,6 +668,3 @@ * Create the config object from the tester config and this item | ||
*/ | ||
config = merge( | ||
config, | ||
itemConfig | ||
); | ||
configs.push(itemConfig); | ||
} | ||
@@ -764,19 +676,40 @@ | ||
let ruleConfig = 1; | ||
if (hasOwnProperty(item, "options")) { | ||
assert(Array.isArray(item.options), "options must be an array"); | ||
if ( | ||
item.options.length > 0 && | ||
typeof rule === "object" && | ||
( | ||
!rule.meta || (rule.meta && (typeof rule.meta.schema === "undefined" || rule.meta.schema === null)) | ||
) | ||
) { | ||
emitMissingSchemaWarning(ruleName); | ||
ruleConfig = [1, ...item.options]; | ||
} | ||
configs.push({ | ||
rules: { | ||
[ruleId]: ruleConfig | ||
} | ||
config.rules[ruleName] = [1].concat(item.options); | ||
} else { | ||
config.rules[ruleName] = 1; | ||
}); | ||
let schema; | ||
try { | ||
schema = getRuleOptionsSchema(rule); | ||
} catch (err) { | ||
err.message += metaSchemaDescription; | ||
throw err; | ||
} | ||
const schema = getRuleOptionsSchema(rule); | ||
/* | ||
* Check and throw an error if the schema is an empty object (`schema:{}`), because such schema | ||
* doesn't validate or enforce anything and is therefore considered a possible error. If the intent | ||
* was to skip options validation, `schema:false` should be set instead (explicit opt-out). | ||
* | ||
* For this purpose, a schema object is considered empty if it doesn't have any own enumerable string-keyed | ||
* properties. While `ajv.compile()` does use enumerable properties from the prototype chain as well, | ||
* it caches compiled schemas by serializing only own enumerable properties, so it's generally not a good idea | ||
* to use inherited properties in schemas because schemas that differ only in inherited properties would end up | ||
* having the same cache entry that would be correct for only one of them. | ||
* | ||
* At this point, `schema` can only be an object or `null`. | ||
*/ | ||
if (schema && Object.keys(schema).length === 0) { | ||
throw new Error(`\`schema: {}\` is a no-op${metaSchemaDescription}`); | ||
} | ||
@@ -788,23 +721,23 @@ /* | ||
*/ | ||
linter.defineRule("rule-tester/validate-ast", { | ||
create() { | ||
return { | ||
Program(node) { | ||
beforeAST = cloneDeeplyExcludesParent(node); | ||
}, | ||
"Program:exit"(node) { | ||
afterAST = node; | ||
configs.push({ | ||
plugins: { | ||
"rule-tester": { | ||
rules: { | ||
"validate-ast": { | ||
create() { | ||
return { | ||
Program(node) { | ||
beforeAST = cloneDeeplyExcludesParent(node); | ||
}, | ||
"Program:exit"(node) { | ||
afterAST = node; | ||
} | ||
}; | ||
} | ||
} | ||
} | ||
}; | ||
} | ||
} | ||
}); | ||
if (typeof config.parser === "string") { | ||
assert(path.isAbsolute(config.parser), "Parsers provided as strings to RuleTester must be absolute paths"); | ||
} else { | ||
config.parser = espreePath; | ||
} | ||
linter.defineParser(config.parser, wrapParser(require(config.parser))); | ||
if (schema) { | ||
@@ -836,6 +769,13 @@ ajv.validateSchema(schema); | ||
validate(config, "rule-tester", id => (id === ruleName ? rule : null)); | ||
// check for validation errors | ||
try { | ||
configs.normalizeSync(); | ||
configs.getConfig("test.js"); | ||
} catch (error) { | ||
error.message = `ESLint configuration in rule-tester is invalid: ${error.message}`; | ||
throw error; | ||
} | ||
// Verify the code. | ||
const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; | ||
const { applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; | ||
const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments"); | ||
@@ -845,3 +785,2 @@ let messages; | ||
try { | ||
SourceCode.prototype.getComments = getCommentsDeprecation; | ||
Object.defineProperty(CodePath.prototype, "currentSegments", { | ||
@@ -855,8 +794,7 @@ get() { | ||
forbiddenMethods.forEach(methodName => { | ||
SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName); | ||
SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName, SourceCode.prototype); | ||
}); | ||
messages = linter.verify(code, config, filename); | ||
messages = linter.verify(code, configs, filename); | ||
} finally { | ||
SourceCode.prototype.getComments = getComments; | ||
Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); | ||
@@ -868,2 +806,3 @@ SourceCode.prototype.applyInlineConfig = applyInlineConfig; | ||
const fatalErrorMessage = messages.find(m => m.fatal); | ||
@@ -876,3 +815,3 @@ | ||
output = SourceCodeFixer.applyFixes(code, messages).output; | ||
const errorMessageInFix = linter.verify(output, config, filename).find(m => m.fatal); | ||
const errorMessageInFix = linter.verify(output, configs, filename).find(m => m.fatal); | ||
@@ -893,3 +832,5 @@ assert(!errorMessageInFix, [ | ||
beforeAST, | ||
afterAST: cloneDeeplyExcludesParent(afterAST) | ||
afterAST: cloneDeeplyExcludesParent(afterAST), | ||
configs, | ||
filename | ||
}; | ||
@@ -983,2 +924,18 @@ } | ||
for (const message of messages) { | ||
if (hasOwnProperty(message, "suggestions")) { | ||
/** @type {Map<string, number>} */ | ||
const seenMessageIndices = new Map(); | ||
for (let i = 0; i < message.suggestions.length; i += 1) { | ||
const suggestionMessage = message.suggestions[i].desc; | ||
const previous = seenMessageIndices.get(suggestionMessage); | ||
assert.ok(!seenMessageIndices.has(suggestionMessage), `Suggestion message '${suggestionMessage}' reported from suggestion ${i} was previously reported by suggestion ${previous}. Suggestion messages should be unique within an error.`); | ||
seenMessageIndices.set(suggestionMessage, i); | ||
} | ||
} | ||
} | ||
if (typeof item.errors === "number") { | ||
@@ -1006,3 +963,3 @@ | ||
const hasMessageOfThisRule = messages.some(m => m.ruleId === ruleName); | ||
const hasMessageOfThisRule = messages.some(m => m.ruleId === ruleId); | ||
@@ -1166,2 +1123,13 @@ for (let i = 0, l = item.errors.length; i < l; i++) { | ||
// Verify if suggestion fix makes a syntax error or not. | ||
const errorMessageInSuggestion = | ||
linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal); | ||
assert(!errorMessageInSuggestion, [ | ||
"A fatal parsing error occurred in suggestion fix.", | ||
`Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`, | ||
"Suggestion output:", | ||
codeWithAppliedSuggestion | ||
].join("\n")); | ||
assert.strictEqual(codeWithAppliedSuggestion, expectedSuggestion.output, `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`); | ||
@@ -1168,0 +1136,0 @@ } |
@@ -276,3 +276,2 @@ /** | ||
"require-await": () => require("./require-await"), | ||
"require-jsdoc": () => require("./require-jsdoc"), | ||
"require-unicode-regexp": () => require("./require-unicode-regexp"), | ||
@@ -300,3 +299,2 @@ "require-yield": () => require("./require-yield"), | ||
"use-isnan": () => require("./use-isnan"), | ||
"valid-jsdoc": () => require("./valid-jsdoc"), | ||
"valid-typeof": () => require("./valid-typeof"), | ||
@@ -303,0 +301,0 @@ "vars-on-top": () => require("./vars-on-top"), |
@@ -443,3 +443,3 @@ /** | ||
description: "Disallow expressions where the operation doesn't affect the value", | ||
recommended: false, | ||
recommended: true, | ||
url: "https://eslint.org/docs/latest/rules/no-constant-binary-expression" | ||
@@ -446,0 +446,0 @@ }, |
@@ -23,3 +23,3 @@ /** | ||
schema: {}, | ||
schema: [], | ||
@@ -26,0 +26,0 @@ fixable: null, |
@@ -18,3 +18,3 @@ /** | ||
description: "Disallow empty static blocks", | ||
recommended: false, | ||
recommended: true, | ||
url: "https://eslint.org/docs/latest/rules/no-empty-static-block" | ||
@@ -21,0 +21,0 @@ }, |
@@ -29,3 +29,3 @@ /** | ||
description: "Disallow unnecessary semicolons", | ||
recommended: true, | ||
recommended: false, | ||
url: "https://eslint.org/docs/latest/rules/no-extra-semi" | ||
@@ -32,0 +32,0 @@ }, |
@@ -15,3 +15,3 @@ /** | ||
const INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/u; | ||
const ALLOWABLE_OPERATORS = ["~", "!!", "+", "*"]; | ||
const ALLOWABLE_OPERATORS = ["~", "!!", "+", "- -", "-", "*"]; | ||
@@ -304,2 +304,10 @@ /** | ||
} | ||
// -(-foo) | ||
operatorAllowed = options.allow.includes("- -"); | ||
if (!operatorAllowed && options.number && node.operator === "-" && node.argument.type === "UnaryExpression" && node.argument.operator === "-" && !isNumeric(node.argument.argument)) { | ||
const recommendation = `Number(${sourceCode.getText(node.argument.argument)})`; | ||
report(node, recommendation, false); | ||
} | ||
}, | ||
@@ -322,2 +330,10 @@ | ||
// foo - 0 | ||
operatorAllowed = options.allow.includes("-"); | ||
if (!operatorAllowed && options.number && node.operator === "-" && node.right.type === "Literal" && node.right.value === 0 && !isNumeric(node.left)) { | ||
const recommendation = `Number(${sourceCode.getText(node.left)})`; | ||
report(node, recommendation, true); | ||
} | ||
// "" + foo | ||
@@ -324,0 +340,0 @@ operatorAllowed = options.allow.includes("+"); |
@@ -52,3 +52,3 @@ /** | ||
description: "Disallow variable or `function` declarations in nested blocks", | ||
recommended: true, | ||
recommended: false, | ||
url: "https://eslint.org/docs/latest/rules/no-inner-declarations" | ||
@@ -55,0 +55,0 @@ }, |
@@ -58,3 +58,3 @@ /** | ||
if (temp) { | ||
allowedFlags = new RegExp(`[${temp}]`, "giu"); | ||
allowedFlags = new RegExp(`[${temp}]`, "gu"); | ||
} | ||
@@ -61,0 +61,0 @@ } |
@@ -21,3 +21,3 @@ /** | ||
description: "Disallow mixed spaces and tabs for indentation", | ||
recommended: true, | ||
recommended: false, | ||
url: "https://eslint.org/docs/latest/rules/no-mixed-spaces-and-tabs" | ||
@@ -24,0 +24,0 @@ }, |
@@ -25,3 +25,3 @@ /** | ||
description: "Disallow `new` operators with global non-constructor functions", | ||
recommended: false, | ||
recommended: true, | ||
url: "https://eslint.org/docs/latest/rules/no-new-native-nonconstructor" | ||
@@ -28,0 +28,0 @@ }, |
/** | ||
* @fileoverview Rule to disallow use of the new operator with the `Symbol` object | ||
* @author Alberto Rodríguez | ||
* @deprecated in ESLint v9.0.0 | ||
*/ | ||
@@ -19,6 +20,12 @@ | ||
description: "Disallow `new` operators with the `Symbol` object", | ||
recommended: true, | ||
recommended: false, | ||
url: "https://eslint.org/docs/latest/rules/no-new-symbol" | ||
}, | ||
deprecated: true, | ||
replacedBy: [ | ||
"no-new-native-nonconstructor" | ||
], | ||
schema: [], | ||
@@ -25,0 +32,0 @@ |
@@ -38,2 +38,3 @@ /** | ||
schema: [{ | ||
type: "object", | ||
properties: { | ||
@@ -40,0 +41,0 @@ allowInParentheses: { |
@@ -19,3 +19,3 @@ /** | ||
description: "Disallow unused private class members", | ||
recommended: false, | ||
recommended: true, | ||
url: "https://eslint.org/docs/latest/rules/no-unused-private-class-members" | ||
@@ -22,0 +22,0 @@ }, |
@@ -21,2 +21,19 @@ /* | ||
//------------------------------------------------------------------------------ | ||
// Typedefs | ||
//------------------------------------------------------------------------------ | ||
/** @typedef {import("../shared/types").Rule} Rule */ | ||
//------------------------------------------------------------------------------ | ||
// Private Members | ||
//------------------------------------------------------------------------------ | ||
// JSON schema that disallows passing any options | ||
const noOptionsSchema = Object.freeze({ | ||
type: "array", | ||
minItems: 0, | ||
maxItems: 0 | ||
}); | ||
//------------------------------------------------------------------------------ | ||
// Requirements | ||
@@ -53,4 +70,6 @@ //------------------------------------------------------------------------------ | ||
* Gets a complete options schema for a rule. | ||
* @param {{create: Function, schema: (Array|null)}} rule A new-style rule object | ||
* @returns {Object} JSON Schema for the rule's options. | ||
* @param {Rule} rule A rule object | ||
* @throws {TypeError} If `meta.schema` is specified but is not an array, object or `false`. | ||
* @returns {Object|null} JSON Schema for the rule's options. | ||
* `null` if rule wasn't passed or its `meta.schema` is `false`. | ||
*/ | ||
@@ -62,5 +81,22 @@ function getRuleOptionsSchema(rule) { | ||
const schema = rule.schema || rule.meta && rule.meta.schema; | ||
if (!rule.meta) { | ||
return { ...noOptionsSchema }; // default if `meta.schema` is not specified | ||
} | ||
// Given a tuple of schemas, insert warning level at the beginning | ||
const schema = rule.meta.schema; | ||
if (typeof schema === "undefined") { | ||
return { ...noOptionsSchema }; // default if `meta.schema` is not specified | ||
} | ||
// `schema:false` is an allowed explicit opt-out of options validation for the rule | ||
if (schema === false) { | ||
return null; | ||
} | ||
if (typeof schema !== "object" || schema === null) { | ||
throw new TypeError("Rule's `meta.schema` must be an array or object"); | ||
} | ||
// ESLint-specific array form needs to be converted into a valid JSON Schema definition | ||
if (Array.isArray(schema)) { | ||
@@ -75,12 +111,9 @@ if (schema.length) { | ||
} | ||
return { | ||
type: "array", | ||
minItems: 0, | ||
maxItems: 0 | ||
}; | ||
// `schema:[]` is an explicit way to specify that the rule does not accept any options | ||
return { ...noOptionsSchema }; | ||
} | ||
// Given a full schema, leave it alone | ||
return schema || null; | ||
// `schema:<object>` is assumed to be a valid JSON Schema definition | ||
return schema; | ||
} | ||
@@ -87,0 +120,0 @@ |
@@ -171,3 +171,3 @@ /** | ||
* @property {Record<string, Processor>} [processors] The definition of plugin processors. | ||
* @property {Record<string, Function | Rule>} [rules] The definition of plugin rules. | ||
* @property {Record<string, Rule>} [rules] The definition of plugin rules. | ||
*/ | ||
@@ -174,0 +174,0 @@ |
@@ -423,5 +423,2 @@ /** | ||
// Cache for comments found using getComments(). | ||
this._commentCache = new WeakMap(); | ||
// don't allow further modification of this object | ||
@@ -477,77 +474,2 @@ Object.freeze(this); | ||
/** | ||
* Gets all comments for the given node. | ||
* @param {ASTNode} node The AST node to get the comments for. | ||
* @returns {Object} An object containing a leading and trailing array | ||
* of comments indexed by their position. | ||
* @public | ||
* @deprecated replaced by getCommentsBefore(), getCommentsAfter(), and getCommentsInside(). | ||
*/ | ||
getComments(node) { | ||
if (this._commentCache.has(node)) { | ||
return this._commentCache.get(node); | ||
} | ||
const comments = { | ||
leading: [], | ||
trailing: [] | ||
}; | ||
/* | ||
* Return all comments as leading comments of the Program node when | ||
* there is no executable code. | ||
*/ | ||
if (node.type === "Program") { | ||
if (node.body.length === 0) { | ||
comments.leading = node.comments; | ||
} | ||
} else { | ||
/* | ||
* Return comments as trailing comments of nodes that only contain | ||
* comments (to mimic the comment attachment behavior present in Espree). | ||
*/ | ||
if ((node.type === "BlockStatement" || node.type === "ClassBody") && node.body.length === 0 || | ||
node.type === "ObjectExpression" && node.properties.length === 0 || | ||
node.type === "ArrayExpression" && node.elements.length === 0 || | ||
node.type === "SwitchStatement" && node.cases.length === 0 | ||
) { | ||
comments.trailing = this.getTokens(node, { | ||
includeComments: true, | ||
filter: isCommentToken | ||
}); | ||
} | ||
/* | ||
* Iterate over tokens before and after node and collect comment tokens. | ||
* Do not include comments that exist outside of the parent node | ||
* to avoid duplication. | ||
*/ | ||
let currentToken = this.getTokenBefore(node, { includeComments: true }); | ||
while (currentToken && isCommentToken(currentToken)) { | ||
if (node.parent && node.parent.type !== "Program" && (currentToken.start < node.parent.start)) { | ||
break; | ||
} | ||
comments.leading.push(currentToken); | ||
currentToken = this.getTokenBefore(currentToken, { includeComments: true }); | ||
} | ||
comments.leading.reverse(); | ||
currentToken = this.getTokenAfter(node, { includeComments: true }); | ||
while (currentToken && isCommentToken(currentToken)) { | ||
if (node.parent && node.parent.type !== "Program" && (currentToken.end > node.parent.end)) { | ||
break; | ||
} | ||
comments.trailing.push(currentToken); | ||
currentToken = this.getTokenAfter(currentToken, { includeComments: true }); | ||
} | ||
} | ||
this._commentCache.set(node, comments); | ||
return comments; | ||
} | ||
/** | ||
* Retrieves the JSDoc comment for a given node. | ||
@@ -968,3 +890,3 @@ * @param {ASTNode} node The AST node to get the comment for. | ||
case "exported": | ||
Object.assign(exportedVariables, commentParser.parseStringConfig(directiveValue, comment)); | ||
Object.assign(exportedVariables, commentParser.parseListConfig(directiveValue, comment)); | ||
break; | ||
@@ -971,0 +893,0 @@ |
@@ -15,5 +15,4 @@ /** | ||
const { FileEnumerator } = require("./cli-engine/file-enumerator"); | ||
const { FlatESLint, shouldUseFlatConfig } = require("./eslint/flat-eslint"); | ||
const FlatRuleTester = require("./rule-tester/flat-rule-tester"); | ||
const { ESLint } = require("./eslint/eslint"); | ||
const { ESLint: FlatESLint, shouldUseFlatConfig } = require("./eslint/eslint"); | ||
const { LegacyESLint } = require("./eslint/legacy-eslint"); | ||
@@ -28,5 +27,4 @@ //----------------------------------------------------------------------------- | ||
shouldUseFlatConfig, | ||
FlatRuleTester, | ||
FileEnumerator, | ||
LegacyESLint: ESLint | ||
LegacyESLint | ||
}; |
{ | ||
"name": "eslint", | ||
"version": "8.56.0", | ||
"version": "9.0.0-alpha.0", | ||
"author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>", | ||
@@ -20,2 +20,3 @@ "description": "An AST-based pattern checker for JavaScript.", | ||
"build:readme": "node tools/update-readme.js", | ||
"build:rules-index": "node Makefile.js generateRuleIndexPage", | ||
"lint": "node Makefile.js lint", | ||
@@ -68,8 +69,7 @@ "lint:docs:js": "node Makefile.js lintDocsJS", | ||
"@eslint-community/regexpp": "^4.6.1", | ||
"@eslint/eslintrc": "^2.1.4", | ||
"@eslint/js": "8.56.0", | ||
"@eslint/eslintrc": "^3.0.0", | ||
"@eslint/js": "9.0.0-alpha.0", | ||
"@humanwhocodes/config-array": "^0.11.13", | ||
"@humanwhocodes/module-importer": "^1.0.1", | ||
"@nodelib/fs.walk": "^1.2.8", | ||
"@ungap/structured-clone": "^1.2.0", | ||
"ajv": "^6.12.4", | ||
@@ -79,3 +79,2 @@ "chalk": "^4.0.0", | ||
"debug": "^4.3.2", | ||
"doctrine": "^3.0.0", | ||
"escape-string-regexp": "^4.0.0", | ||
@@ -97,3 +96,2 @@ "eslint-scope": "^7.2.2", | ||
"is-path-inside": "^3.0.3", | ||
"js-yaml": "^4.1.0", | ||
"json-stable-stringify-without-jsonify": "^1.0.1", | ||
@@ -128,4 +126,4 @@ "levn": "^0.4.1", | ||
"eslint-plugin-internal-rules": "file:tools/internal-rules", | ||
"eslint-plugin-jsdoc": "^46.2.5", | ||
"eslint-plugin-n": "^16.4.0", | ||
"eslint-plugin-jsdoc": "^46.9.0", | ||
"eslint-plugin-n": "^16.6.0", | ||
"eslint-plugin-unicorn": "^49.0.0", | ||
@@ -140,2 +138,3 @@ "eslint-release": "^3.2.0", | ||
"gray-matter": "^4.0.3", | ||
"js-yaml": "^4.1.0", | ||
"lint-staged": "^11.0.0", | ||
@@ -146,3 +145,3 @@ "load-perf": "^0.2.0", | ||
"markdownlint": "^0.32.0", | ||
"markdownlint-cli": "^0.37.0", | ||
"markdownlint-cli": "^0.38.0", | ||
"marked": "^4.0.8", | ||
@@ -157,3 +156,2 @@ "memfs": "^3.0.1", | ||
"mocha": "^8.3.2", | ||
"mocha-junit-reporter": "^2.0.0", | ||
"node-polyfill-webpack-plugin": "^1.0.3", | ||
@@ -185,4 +183,4 @@ "npm-license": "^0.3.3", | ||
"engines": { | ||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" | ||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||
} | ||
} |
@@ -46,3 +46,3 @@ [![npm version](https://img.shields.io/npm/v/eslint.svg)](https://www.npmjs.com/package/eslint) | ||
Prerequisites: [Node.js](https://nodejs.org/) (`^12.22.0`, `^14.17.0`, or `>=16.0.0`) built with SSL support. (If you are using an official Node.js distribution, SSL is always built in.) | ||
Prerequisites: [Node.js](https://nodejs.org/) (`^18.18.0`, `^20.9.0`, or `>=21.1.0`) built with SSL support. (If you are using an official Node.js distribution, SSL is always built in.) | ||
@@ -297,3 +297,3 @@ You can install and configure ESLint using this command: | ||
<p><a href="https://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a></p><h3>Silver Sponsors</h3> | ||
<p><a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a> <a href="https://www.workleap.com"><img src="https://avatars.githubusercontent.com/u/53535748?u=d1e55d7661d724bf2281c1bfd33cb8f99fe2465f&v=4" alt="Workleap" height="64"></a></p><h3>Bronze Sponsors</h3> | ||
<p><a href="https://www.jetbrains.com/"><img src="https://images.opencollective.com/jetbrains/eb04ddc/logo.png" alt="JetBrains" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a> <a href="https://www.workleap.com"><img src="https://avatars.githubusercontent.com/u/53535748?u=d1e55d7661d724bf2281c1bfd33cb8f99fe2465f&v=4" alt="Workleap" height="64"></a></p><h3>Bronze Sponsors</h3> | ||
<p><a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://transloadit.com/"><img src="https://avatars.githubusercontent.com/u/125754?v=4" alt="Transloadit" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104?v=4" alt="Nx" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a></p> | ||
@@ -300,0 +300,0 @@ <!--sponsorsend--> |
Sorry, the diff of this file is too big to display
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
35
15
2956519
398
69708
1
+ Added@eslint/eslintrc@3.0.2(transitive)
+ Added@eslint/js@9.0.0-alpha.09.1.1(transitive)
+ Added@humanwhocodes/config-array@0.13.0(transitive)
+ Added@humanwhocodes/retry@0.2.3(transitive)
+ Addedeslint@9.1.1(transitive)
+ Addedeslint-scope@8.0.1(transitive)
+ Addedeslint-visitor-keys@4.0.0(transitive)
+ Addedespree@10.0.1(transitive)
+ Addedfile-entry-cache@8.0.0(transitive)
+ Addedflat-cache@4.0.1(transitive)
+ Addedglobals@14.0.0(transitive)
- Removed@ungap/structured-clone@^1.2.0
- Removeddoctrine@^3.0.0
- Removedjs-yaml@^4.1.0
- Removed@eslint/eslintrc@2.1.4(transitive)
- Removed@eslint/js@8.56.0(transitive)
- Removed@ungap/structured-clone@1.2.0(transitive)
- Removeddoctrine@3.0.0(transitive)
Updated@eslint/eslintrc@^3.0.0
Updated@eslint/js@9.0.0-alpha.0