From c8d17e11dc63909e693eaed5b5ccc50e698ac3b3 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Sat, 3 May 2025 08:08:22 +0000 Subject: [PATCH 01/36] docs: Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19d529d94206..595402b06b47 100644 --- a/README.md +++ b/README.md @@ -324,7 +324,7 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Automattic Airbnb

Gold Sponsors

Qlty Software trunk.io Shopify

Silver Sponsors

Vite Liftoff American Express StackBlitz

Bronze Sponsors

-

Icons8 Discord GitBook Neko Nx Mercedes-Benz Group HeroCoders LambdaTest

+

Cybozu Anagram Solver Icons8 Discord GitBook Neko Nx Mercedes-Benz Group HeroCoders LambdaTest

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

From a3a255924866b94ef8d604e91636547600edec56 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sun, 4 May 2025 18:59:22 +0200 Subject: [PATCH 02/36] docs: fix wording in Combine Configs (#19685) --- docs/src/use/configure/combine-configs.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/use/configure/combine-configs.md b/docs/src/use/configure/combine-configs.md index cf2c33ee956a..89aee283f460 100644 --- a/docs/src/use/configure/combine-configs.md +++ b/docs/src/use/configure/combine-configs.md @@ -11,7 +11,7 @@ In many cases, you won't write an ESLint config file from scratch, but rather, y ## Apply a Config Object -If you are importing an object from another module, in most cases, you can just insert the object directly into your config file's exported array. For example, you can use the recommended rule configurations for JavaScript by importing the `recommended` config and using it in your array: +If you are importing an object from another module, in most cases, you can just pass the object directly to the `defineConfig()` helper. For example, you can use the recommended rule configurations for JavaScript by importing the `recommended` config and using it in your array: ```js // eslint.config.js @@ -54,7 +54,7 @@ Here, the `js/recommended` config object is applied only to files that match the ## Apply a Config Array -If you are importing an array from another module, insert the array directly into your exported array. Here's an example: +If you are importing an array from another module, pass the array directly to the `defineConfig()` helper. Here's an example: ```js // eslint.config.js From eb316a83a49347ab47ae965ff95f81dd620d074c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Mon, 5 May 2025 04:27:29 +0900 Subject: [PATCH 03/36] docs: add `fmt` and `check` sections to `Package.json Conventions` (#19686) --- docs/src/contribute/package-json-conventions.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/src/contribute/package-json-conventions.md b/docs/src/contribute/package-json-conventions.md index f347f154d733..16327785b46a 100644 --- a/docs/src/contribute/package-json-conventions.md +++ b/docs/src/contribute/package-json-conventions.md @@ -18,7 +18,7 @@ Here is a summary of the proposal in ABNF. ```abnf name = life-cycle / main target? option* ":watch"? life-cycle = "prepare" / "preinstall" / "install" / "postinstall" / "prepublish" / "preprepare" / "prepare" / "postprepare" / "prepack" / "postpack" / "prepublishOnly" -main = "build" / "lint" ":fix"? / "release" / "start" / "test" / "fetch" +main = "build" / "lint" ":fix"? / "fmt" ":check"? / "release" / "start" / "test" / "fetch" target = ":" word ("-" word)* / extension ("+" extension)* option = ":" word ("-" word)* word = ALPHA + @@ -27,7 +27,7 @@ extension = ( ALPHA / DIGIT )+ ## Order -The script names MUST appear in the package.json file in alphabetical order. The other conventions outlined in this document ensure that alphabetical order will coincide with logical groupings. +The script names MUST appear in the `package.json` file in alphabetical order. The other conventions outlined in this document ensure that alphabetical order will coincide with logical groupings. ## Main Script Names @@ -47,7 +47,7 @@ If a package contains any `fetch:*` scripts, there MAY be a script named `fetch` ### Release -Scripts that have public side effects (publishing the web site, committing to Git, etc.) MUST begin with `release`. +Scripts that have public side effects (publishing the website, committing to Git, etc.) MUST begin with `release`. ### Lint @@ -57,6 +57,12 @@ If a package contains any `lint:*` scripts, there SHOULD be a script named `lint If fixing is available, a linter MUST NOT apply fixes UNLESS the script contains the `:fix` modifier (see below). +### Fmt + +Scripts that format source code MUST have names that begin with `fmt`. + +If a package contains any `fmt:*` scripts, there SHOULD be a script named `fmt` that applies formatting fixes to all source files. There SHOULD also be a script named `fmt:check` that validates code formatting without modifying files and exits non-zero if any files are out of compliance. + ### Start A `start` script is used to start a server. As of this writing, no ESLint package has more than one `start` script, so there's no need `start` to have any modifiers. @@ -79,6 +85,10 @@ One or more of the following modifiers MAY be appended to the standard script na If it's possible for a linter to fix problems that it finds, add a copy of the script with `:fix` appended to the end that also fixes. +### Check + +If a script validates code or artifacts without making any modifications, append `:check` to the script name. This modifier is typically used for formatters (e.g., `fmt:check`) to verify that files conform to the expected format and to exit with a non-zero status if any issues are found. Scripts with the `:check` modifier MUST NOT alter any files or outputs. + ### Target The name of the target of the action being run. In the case of a `build` script, it SHOULD identify the build artifact(s), e.g. "javascript" or "css" or "website". In the case of a `lint` or `test` script, it SHOULD identify the item(s) being linted or tested. In the case of a `start` script, it SHOULD identify which server is starting. From f305beb82c51215ad48c5c860f02be1b34bcce32 Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Mon, 5 May 2025 08:17:00 +0200 Subject: [PATCH 04/36] test: mock `process.emitWarning` to prevent output disruption (#19687) * test: mock `process.emitWarning` to prevent output disruption * remove unnecessary `async` Co-authored-by: Milos Djermanovic * add `callThrough()` and `returns()` --------- Co-authored-by: Milos Djermanovic --- tests/lib/mcp/mcp-server.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/lib/mcp/mcp-server.js b/tests/lib/mcp/mcp-server.js index f15d1458b75b..ee5e7b6de2c4 100644 --- a/tests/lib/mcp/mcp-server.js +++ b/tests/lib/mcp/mcp-server.js @@ -14,6 +14,7 @@ const assert = require("chai").assert; const path = require("node:path"); const { Client } = require("@modelcontextprotocol/sdk/client/index.js"); const { InMemoryTransport } = require("@modelcontextprotocol/sdk/inMemory.js"); +const sinon = require("sinon"); //----------------------------------------------------------------------------- // Helpers @@ -55,6 +56,16 @@ describe("MCP Server", () => { // Note: must connect server first or else client hangs await mcpServer.connect(serverTransport); await client.connect(clientTransport); + + sinon + .stub(process, "emitWarning") + .callThrough() + .withArgs(sinon.match.any, "ESLintIgnoreWarning") + .returns(); + }); + + afterEach(() => { + sinon.restore(); }); describe("Tools", () => { From 8ed32734cc22988173f99fd0703d50f94c60feb8 Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Mon, 5 May 2025 09:18:59 +0200 Subject: [PATCH 05/36] docs: fix internal usages of `ConfigData` type (#19688) --- docs/src/integrate/nodejs-api.md | 4 ++-- lib/cli-engine/cli-engine.js | 4 ++-- lib/config/config-loader.js | 28 +++++++++---------------- lib/eslint/eslint.js | 9 ++++---- lib/eslint/legacy-eslint.js | 2 +- lib/linter/linter.js | 13 ++++++------ lib/shared/types.js | 36 -------------------------------- tools/eslint-fuzzer.js | 6 ++++++ 8 files changed, 33 insertions(+), 69 deletions(-) diff --git a/docs/src/integrate/nodejs-api.md b/docs/src/integrate/nodejs-api.md index 0f7e5c14706b..f3e91febbd1b 100644 --- a/docs/src/integrate/nodejs-api.md +++ b/docs/src/integrate/nodejs-api.md @@ -140,9 +140,9 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob - `options.allowInlineConfig` (`boolean`)
Default is `true`. If `false` is present, ESLint suppresses directive comments in source code. If this option is `false`, it overrides the `noInlineConfig` setting in your configurations. -- `options.baseConfig` (`ConfigData | ConfigData[] | null`)
+- `options.baseConfig` (`Config | Config[] | null`)
Default is `null`. [Configuration object], extended by all configurations used with this instance. You can use this option to define the default settings that will be used if your configuration files don't configure it. -- `options.overrideConfig` (`ConfigData | ConfigData[] | null`)
+- `options.overrideConfig` (`Config | Config[] | null`)
Default is `null`. [Configuration object], added after any existing configuration and therefore applies after what's contained in your configuration file (if used). - `options.overrideConfigFile` (`null | true | string`)
Default is `null`. By default, ESLint searches for a configuration file. When this option is set to `true`, ESLint does not search for a configuration file. When this option is set to a `string` value, ESLint does not search for a configuration file, and uses the provided value as the path to the configuration file. diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 3c9537cceb2f..ffa5c34c1153 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -58,15 +58,15 @@ const validFixTypes = new Set(["directive", "problem", "suggestion", "layout"]); //------------------------------------------------------------------------------ // For VSCode IntelliSense -/** @typedef {import("../shared/types").ConfigData} ConfigData */ /** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ /** @typedef {import("../shared/types").LintMessage} LintMessage */ /** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */ /** @typedef {import("../shared/types").ParserOptions} ParserOptions */ /** @typedef {import("../shared/types").RuleConf} RuleConf */ -/** @typedef {import("../types").Rule.RuleModule} Rule */ +/** @typedef {import("../types").ESLint.ConfigData} ConfigData */ /** @typedef {import("../types").ESLint.FormatterFunction} FormatterFunction */ /** @typedef {import("../types").ESLint.Plugin} Plugin */ +/** @typedef {import("../types").Rule.RuleModule} Rule */ /** @typedef {ReturnType} ConfigArray */ /** @typedef {ReturnType} ExtractedConfig */ diff --git a/lib/config/config-loader.js b/lib/config/config-loader.js index 39908f79fa5b..3557f5cc8954 100644 --- a/lib/config/config-loader.js +++ b/lib/config/config-loader.js @@ -20,19 +20,17 @@ const { FlatConfigArray } = require("./flat-config-array"); // Types //----------------------------------------------------------------------------- -/** - * @import { ConfigData, ConfigData as FlatConfigObject } from "../shared/types.js"; - */ +/** @typedef {import("../types").Linter.Config} Config */ /** * @typedef {Object} ConfigLoaderOptions * @property {string|false|undefined} configFile The path to the config file to use. * @property {string} cwd The current working directory. * @property {boolean} ignoreEnabled Indicates if ignore patterns should be honored. - * @property {FlatConfigArray} [baseConfig] The base config to use. - * @property {Array} [defaultConfigs] The default configs to use. + * @property {Config|Array} [baseConfig] The base config to use. + * @property {Array} [defaultConfigs] The default configs to use. * @property {Array} [ignorePatterns] The ignore patterns to use. - * @property {FlatConfigObject|Array} [overrideConfig] The override config to use. + * @property {Config|Array} [overrideConfig] The override config to use. * @property {boolean} [hasUnstableNativeNodeJsTSConfigFlag] The flag to indicate whether the `unstable_native_nodejs_ts_config` flag is enabled. */ @@ -394,8 +392,7 @@ class ConfigLoader { * This is the same logic used by the ESLint CLI executable to determine * configuration for each file it processes. * @param {string} filePath The path of the file or directory to retrieve config for. - * @returns {Promise} A configuration object for the file - * or `undefined` if there is no configuration data for the file. + * @returns {Promise} A configuration object for the file. * @throws {Error} If no configuration for `filePath` exists. */ async loadConfigArrayForFile(filePath) { @@ -415,8 +412,7 @@ class ConfigLoader { * This is the same logic used by the ESLint CLI executable to determine * configuration for each file it processes. * @param {string} dirPath The path of the directory to retrieve config for. - * @returns {Promise} A configuration object for the directory - * or `undefined` if there is no configuration data for the directory. + * @returns {Promise} A configuration object for the directory. */ async loadConfigArrayForDirectory(dirPath) { assertValidFilePath(dirPath); @@ -440,8 +436,7 @@ class ConfigLoader { * intended to be used in locations where we know the config file has already * been loaded and we just need to get the configuration for a file. * @param {string} filePath The path of the file to retrieve a config object for. - * @returns {ConfigData|undefined} A configuration object for the file - * or `undefined` if there is no configuration data for the file. + * @returns {FlatConfigArray} A configuration object for the file. * @throws {Error} If `filePath` is not a non-empty string. * @throws {Error} If `filePath` is not an absolute path. * @throws {Error} If the config file was not already loaded. @@ -460,8 +455,7 @@ class ConfigLoader { * intended to be used in locations where we know the config file has already * been loaded and we just need to get the configuration for a file. * @param {string} fileOrDirPath The path of the directory to retrieve a config object for. - * @returns {ConfigData|undefined} A configuration object for the directory - * or `undefined` if there is no configuration data for the directory. + * @returns {FlatConfigArray} A configuration object for the directory. * @throws {Error} If `dirPath` is not a non-empty string. * @throws {Error} If `dirPath` is not an absolute path. * @throws {Error} If the config file was not already loaded. @@ -789,8 +783,7 @@ class LegacyConfigLoader extends ConfigLoader { * This is the same logic used by the ESLint CLI executable to determine * configuration for each file it processes. * @param {string} dirPath The path of the directory to retrieve config for. - * @returns {Promise} A configuration object for the file - * or `undefined` if there is no configuration data for the file. + * @returns {Promise} A configuration object for the file. */ async loadConfigArrayForDirectory(dirPath) { assertValidFilePath(dirPath); @@ -812,8 +805,7 @@ class LegacyConfigLoader extends ConfigLoader { * intended to be used in locations where we know the config file has already * been loaded and we just need to get the configuration for a file. * @param {string} dirPath The path of the directory to retrieve a config object for. - * @returns {ConfigData|undefined} A configuration object for the file - * or `undefined` if there is no configuration data for the file. + * @returns {FlatConfigArray} A configuration object for the file. * @throws {Error} If `dirPath` is not a non-empty string. * @throws {Error} If `dirPath` is not an absolute path. * @throws {Error} If the config file was not already loaded. diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index e4a30c60a8c6..d9e9348bc14d 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -57,17 +57,18 @@ const { ConfigLoader, LegacyConfigLoader } = require("../config/config-loader"); * @import { CLIEngineLintReport } from "./legacy-eslint.js"; * @import { FlatConfigArray } from "../config/flat-config-array.js"; * @import { RuleDefinition } from "@eslint/core"; - * @import { ConfigData, DeprecatedRuleInfo, LintMessage, LintResult, ResultsMeta } from "../shared/types.js"; + * @import { DeprecatedRuleInfo, LintMessage, LintResult, ResultsMeta } from "../shared/types.js"; */ /** @typedef {ReturnType} ExtractedConfig */ +/** @typedef {import("../types").Linter.Config} Config */ /** @typedef {import("../types").ESLint.Plugin} Plugin */ /** * The options with which to configure the ESLint instance. * @typedef {Object} ESLintOptions * @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments. - * @property {ConfigData|Array} [baseConfig] Base config, extended by all configs used with this instance + * @property {Config|Array} [baseConfig] Base config, extended by all configs used with this instance * @property {boolean} [cache] Enable result caching. * @property {string} [cacheLocation] The cache file to use instead of .eslintcache. * @property {"metadata" | "content"} [cacheStrategy] The strategy used to detect changed files. @@ -79,7 +80,7 @@ const { ConfigLoader, LegacyConfigLoader } = require("../config/config-loader"); * @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 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|Array} [overrideConfig] Override config, overrides all configs used with this instance + * @property {Config|Array} [overrideConfig] Override config, overrides all configs used with this instance * @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. @@ -1068,7 +1069,7 @@ class ESLint { * This is the same logic used by the ESLint CLI executable to determine * configuration for each file it processes. * @param {string} filePath The path of the file to retrieve a config object for. - * @returns {Promise} A configuration object for the file + * @returns {Promise} A configuration object for the file * or `undefined` if there is no configuration data for the object. */ async calculateConfigForFile(filePath) { diff --git a/lib/eslint/legacy-eslint.js b/lib/eslint/legacy-eslint.js index f5ddd712d8ab..72caa83a4fed 100644 --- a/lib/eslint/legacy-eslint.js +++ b/lib/eslint/legacy-eslint.js @@ -31,11 +31,11 @@ const { version } = require("../../package.json"); /** @typedef {import("../cli-engine/cli-engine").LintReport} CLIEngineLintReport */ /** @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").ResultsMeta} ResultsMeta */ +/** @typedef {import("../types").ESLint.ConfigData} ConfigData */ /** @typedef {import("../types").ESLint.Plugin} Plugin */ /** @typedef {import("../types").Rule.RuleModule} Rule */ diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 8b516558ce3d..85982616b583 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -75,7 +75,6 @@ const STEP_KIND_CALL = 2; /** @import { Language, LanguageOptions, RuleConfig, RuleDefinition, RuleSeverity } from "@eslint/core" */ -/** @typedef {import("../shared/types").ConfigData} ConfigData */ /** @typedef {import("../shared/types").Environment} Environment */ /** @typedef {import("../shared/types").GlobalConf} GlobalConf */ /** @typedef {import("../shared/types").LintMessage} LintMessage */ @@ -83,6 +82,8 @@ const STEP_KIND_CALL = 2; /** @typedef {import("../shared/types").ParserOptions} ParserOptions */ /** @typedef {import("../shared/types").Processor} Processor */ /** @typedef {import("../shared/types").Times} Times */ +/** @typedef {import("../types").Linter.Config} Config */ +/** @typedef {import("../types").ESLint.ConfigData} ConfigData */ /** @typedef {import("../types").Linter.LanguageOptions} JSLanguageOptions */ /** @typedef {import("../types").Linter.StringSeverity} StringSeverity */ /** @typedef {import("../types").Rule.RuleModule} Rule */ @@ -350,7 +351,7 @@ function asArray(value) { /** * Pushes a problem to inlineConfigProblems if ruleOptions are redundant. - * @param {ConfigData} config Provided config. + * @param {Config} config Provided config. * @param {Object} loc A line/column location * @param {Array} problems Problems that may be added to. * @param {string} ruleId The rule ID. @@ -901,7 +902,7 @@ function normalizeFilename(filename) { * Normalizes the possible options for `linter.verify` and `linter.verifyAndFix` to a * consistent shape. * @param {VerifyOptions} providedOptions Options - * @param {ConfigData} config Config. + * @param {Config|ConfigData} config Config. * @returns {Required & InternalOptions} Normalized options */ function normalizeVerifyOptions(providedOptions, config) { @@ -1885,7 +1886,7 @@ class Linter { /** * Verify with a processor. * @param {string|SourceCode} textOrSourceCode The source code. - * @param {FlatConfig} config The config array. + * @param {Config} config The config array. * @param {VerifyOptions&ProcessorOptions} options The options. * @param {FlatConfigArray} [configForRecursive] The `ConfigArray` object to apply multiple processors recursively. * @returns {(LintMessage|SuppressedLintMessage)[]} The found problems. @@ -1986,7 +1987,7 @@ class Linter { /** * Verify using flat config and without any processors. * @param {VFile} file The file to lint. - * @param {FlatConfig} providedConfig An ESLintConfig instance to configure everything. + * @param {Config} providedConfig An ESLintConfig instance to configure everything. * @param {VerifyOptions} [providedOptions] The optional filename of the file being checked. * @throws {Error} If during rule execution. * @returns {(LintMessage|SuppressedLintMessage)[]} The results as an array of messages or an empty array if no messages. @@ -2348,7 +2349,7 @@ class Linter { /** * Same as linter.verify, except without support for processors. * @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object. - * @param {FlatConfig} providedConfig An ESLintConfig instance to configure everything. + * @param {Config} providedConfig An ESLintConfig instance to configure everything. * @param {VerifyOptions} [providedOptions] The optional filename of the file being checked. * @throws {Error} If during rule execution. * @returns {(LintMessage|SuppressedLintMessage)[]} The results as an array of messages or an empty array if no messages. diff --git a/lib/shared/types.js b/lib/shared/types.js index b627d920b4ea..ad253a4e1b20 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -26,42 +26,6 @@ module.exports = {}; * @property {boolean} [allowReserved] Allowing the use of reserved words as identifiers in ES3. */ -/** - * @typedef {Object} ConfigData - * @property {Record} [env] The environment settings. - * @property {string | string[]} [extends] The path to other config files or the package name of shareable configs. - * @property {Record} [globals] The global variable settings. - * @property {string | string[]} [ignorePatterns] The glob patterns that ignore to lint. - * @property {boolean} [noInlineConfig] The flag that disables directive comments. - * @property {OverrideConfigData[]} [overrides] The override settings per kind of files. - * @property {string} [parser] The path to a parser or the package name of a parser. - * @property {ParserOptions} [parserOptions] The parser options. - * @property {string[]} [plugins] The plugin specifiers. - * @property {string} [processor] The processor specifier. - * @property {boolean} [reportUnusedDisableDirectives] The flag to report unused `eslint-disable` comments. - * @property {boolean} [root] The root flag. - * @property {Record} [rules] The rule settings. - * @property {Object} [settings] The shared settings. - */ - -/** - * @typedef {Object} OverrideConfigData - * @property {Record} [env] The environment settings. - * @property {string | string[]} [excludedFiles] The glob patterns for excluded files. - * @property {string | string[]} [extends] The path to other config files or the package name of shareable configs. - * @property {string | string[]} files The glob patterns for target files. - * @property {Record} [globals] The global variable settings. - * @property {boolean} [noInlineConfig] The flag that disables directive comments. - * @property {OverrideConfigData[]} [overrides] The override settings per kind of files. - * @property {string} [parser] The path to a parser or the package name of a parser. - * @property {ParserOptions} [parserOptions] The parser options. - * @property {string[]} [plugins] The plugin specifiers. - * @property {string} [processor] The processor specifier. - * @property {boolean} [reportUnusedDisableDirectives] The flag to report unused `eslint-disable` comments. - * @property {Record} [rules] The rule settings. - * @property {Object} [settings] The shared settings. - */ - /** * @typedef {Object} ParseResult * @property {Object} ast The AST. diff --git a/tools/eslint-fuzzer.js b/tools/eslint-fuzzer.js index 7d61728354a1..9297a57b7491 100644 --- a/tools/eslint-fuzzer.js +++ b/tools/eslint-fuzzer.js @@ -16,6 +16,12 @@ const SourceCodeFixer = require("../lib/linter/source-code-fixer"); const ruleConfigs = require("./config-rule").createCoreRuleConfigs(true); const sampleMinimizer = require("./code-sample-minimizer"); +//------------------------------------------------------------------------------ +// Typedefs +//------------------------------------------------------------------------------ + +/** @typedef {import("../lib/types").ESLint.ConfigData} ConfigData */ + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ From 35304dd2b0d8a4b640b9a25ae27ebdcb5e124cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Tue, 6 May 2025 14:19:36 +0900 Subject: [PATCH 06/36] chore: add missing `funding` field to packages (#19684) --- packages/eslint-config-eslint/package.json | 1 + packages/js/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/eslint-config-eslint/package.json b/packages/eslint-config-eslint/package.json index 60c4f675c077..f753bffb720c 100644 --- a/packages/eslint-config-eslint/package.json +++ b/packages/eslint-config-eslint/package.json @@ -3,6 +3,7 @@ "version": "11.0.0", "author": "Nicholas C. Zakas ", "description": "Default ESLint configuration for ESLint projects.", + "funding": "https://eslint.org/donate", "main": "./index.js", "types": "./types/index.d.ts", "exports": { diff --git a/packages/js/package.json b/packages/js/package.json index 4b296314d440..c535438f8cf1 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -2,6 +2,7 @@ "name": "@eslint/js", "version": "9.26.0", "description": "ESLint JavaScript language implementation", + "funding": "https://eslint.org/donate", "main": "./src/index.js", "types": "./types/index.d.ts", "scripts": { From 32957cde72196c7e41741db311786d881c1613a1 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Tue, 6 May 2025 21:33:55 +0530 Subject: [PATCH 07/36] feat: support TS syntax in `max-params` (#19557) * feat: support TS syntax in `max-params` * docs: add countVoidThis option * docs: add countVoidThis option * chore: fix lint * docs: udpate examples * docs: revert formatting * refactor: simplify logic * refactor: simplify logic * docs: fix examples * docs: update docs/src/rules/max-params.md Co-authored-by: Milos Djermanovic * docs: update types * test: add more test cases * test: add more test cases * chore: update tests * chore: remove unwanted changes --------- Co-authored-by: Milos Djermanovic --- docs/src/rules/max-params.md | 61 +++++++++++++ lib/rules/max-params.js | 39 ++++++-- lib/types/rules.d.ts | 4 + tests/lib/rules/max-params.js | 163 ++++++++++++++++++++++++++++++++++ 4 files changed, 260 insertions(+), 7 deletions(-) diff --git a/docs/src/rules/max-params.md b/docs/src/rules/max-params.md index 92d7dde8dfe5..456729237eea 100644 --- a/docs/src/rules/max-params.md +++ b/docs/src/rules/max-params.md @@ -29,6 +29,7 @@ This rule enforces a maximum number of parameters allowed in function definition This rule has a number or object option: * `"max"` (default `3`) enforces a maximum number of parameters in function definitions +* `"countVoidThis"` (default `false`) counts a `this` declaration when the type is `void` (TypeScript only) **Deprecated:** The object property `maximum` is deprecated; please use the object property `max` instead. @@ -69,3 +70,63 @@ let foo2 = (bar, baz, qux) => { ``` ::: + +### countVoidThis (TypeScript only) + +This rule has a TypeScript-specific option `countVoidThis` that allows you to count a `this` declaration when the type is `void`. + +Examples of **correct** TypeScript code for this rule with the default `{ "countVoidThis": false }` option: + +:::correct + +```ts +/*eslint max-params: ["error", { "max": 2, "countVoidThis": false }]*/ + +function hasNoThis(this: void, first: string, second: string) { + // ... +} +``` + +::: + +Examples of **incorrect** TypeScript code for this rule with the default `{ "countVoidThis": false }` option: + +:::incorrect + +```ts +/*eslint max-params: ["error", { "max": 2, "countVoidThis": false }]*/ + +function hasNoThis(this: void, first: string, second: string, third: string) { + // ... +} +``` + +::: + +Examples of **correct** TypeScript code for this rule with the `{ "countVoidThis": true }` option: + +:::correct + +```ts +/*eslint max-params: ["error", { "max": 2, "countVoidThis": true }]*/ + +function hasNoThis(this: void, first: string) { + // ... +} +``` + +::: + +Examples of **incorrect** TypeScript code for this rule with the `{ "countVoidThis": true }` option: + +:::incorrect + +```ts +/*eslint max-params: ["error", { "max": 2, "countVoidThis": true }]*/ + +function hasNoThis(this: void, first: string, second: string) { + // ... +} +``` + +::: diff --git a/lib/rules/max-params.js b/lib/rules/max-params.js index f385cfad28bf..eeb61edd4e62 100644 --- a/lib/rules/max-params.js +++ b/lib/rules/max-params.js @@ -20,6 +20,8 @@ const { upperCaseFirst } = require("../shared/string-utils"); module.exports = { meta: { type: "suggestion", + dialects: ["typescript", "javascript"], + language: "javascript", docs: { description: @@ -46,6 +48,11 @@ module.exports = { type: "integer", minimum: 0, }, + countVoidThis: { + type: "boolean", + description: + "Whether to count a `this` declaration when the type is `void`.", + }, }, additionalProperties: false, }, @@ -61,12 +68,16 @@ module.exports = { const sourceCode = context.sourceCode; const option = context.options[0]; let numParams = 3; + let countVoidThis = false; - if ( - typeof option === "object" && - (Object.hasOwn(option, "maximum") || Object.hasOwn(option, "max")) - ) { - numParams = option.maximum || option.max; + if (typeof option === "object") { + if ( + Object.hasOwn(option, "maximum") || + Object.hasOwn(option, "max") + ) { + numParams = option.maximum || option.max; + } + countVoidThis = option.countVoidThis; } if (typeof option === "number") { numParams = option; @@ -79,7 +90,19 @@ module.exports = { * @private */ function checkFunction(node) { - if (node.params.length > numParams) { + const hasVoidThisParam = + node.params.length > 0 && + node.params[0].type === "Identifier" && + node.params[0].name === "this" && + node.params[0].typeAnnotation?.typeAnnotation.type === + "TSVoidKeyword"; + + const effectiveParamCount = + hasVoidThisParam && !countVoidThis + ? node.params.length - 1 + : node.params.length; + + if (effectiveParamCount > numParams) { context.report({ loc: astUtils.getFunctionHeadLoc(node, sourceCode), node, @@ -88,7 +111,7 @@ module.exports = { name: upperCaseFirst( astUtils.getFunctionNameWithKind(node), ), - count: node.params.length, + count: effectiveParamCount, max: numParams, }, }); @@ -99,6 +122,8 @@ module.exports = { FunctionDeclaration: checkFunction, ArrowFunctionExpression: checkFunction, FunctionExpression: checkFunction, + TSDeclareFunction: checkFunction, + TSFunctionType: checkFunction, }; }, }; diff --git a/lib/types/rules.d.ts b/lib/types/rules.d.ts index cd3fea23612a..91524a6cb536 100644 --- a/lib/types/rules.d.ts +++ b/lib/types/rules.d.ts @@ -1800,6 +1800,10 @@ export interface ESLintRules extends Linter.RulesRecord { * @default 3 */ max: number; + /** + * @default false + */ + countVoidThis: boolean; }> | number, ] diff --git a/tests/lib/rules/max-params.js b/tests/lib/rules/max-params.js index 030006fef723..6401453ccdbd 100644 --- a/tests/lib/rules/max-params.js +++ b/tests/lib/rules/max-params.js @@ -151,3 +151,166 @@ ruleTester.run("max-params", rule, { }, ], }); + +const ruleTesterTypeScript = new RuleTester({ + languageOptions: { + parser: require("@typescript-eslint/parser"), + }, +}); + +ruleTesterTypeScript.run("max-params", rule, { + valid: [ + "function foo() {}", + "const foo = function () {};", + "const foo = () => {};", + "function foo(a) {}", + ` + class Foo { + constructor(a) {} + } + `, + ` + class Foo { + method(this: void, a, b, c) {} + } + `, + ` + class Foo { + method(this: Foo, a, b) {} + } + `, + { + code: "function foo(a, b, c, d) {}", + options: [{ max: 4 }], + }, + { + code: "function foo(a, b, c, d) {}", + options: [{ maximum: 4 }], + }, + { + code: ` + class Foo { + method(this: void) {} + } + `, + options: [{ max: 0 }], + }, + { + code: ` + class Foo { + method(this: void, a) {} + } + `, + options: [{ max: 1 }], + }, + { + code: ` + class Foo { + method(this: void, a) {} + } + `, + options: [{ countVoidThis: true, max: 2 }], + }, + { + code: `function testD(this: void, a) {}`, + options: [{ max: 1 }], + }, + { + code: `function testD(this: void, a) {}`, + options: [{ countVoidThis: true, max: 2 }], + }, + { + code: `const testE = function (this: void, a) {}`, + options: [{ max: 1 }], + }, + { + code: `const testE = function (this: void, a) {}`, + options: [{ countVoidThis: true, max: 2 }], + }, + { + code: ` + declare function makeDate(m: number, d: number, y: number): Date; + `, + options: [{ max: 3 }], + }, + { + code: ` + type sum = (a: number, b: number) => number; + `, + options: [{ max: 2 }], + }, + ], + invalid: [ + { + code: "function foo(a, b, c, d) {}", + errors: [{ messageId: "exceed" }], + }, + { + code: "const foo = function (a, b, c, d) {};", + errors: [{ messageId: "exceed" }], + }, + { + code: "const foo = (a, b, c, d) => {};", + errors: [{ messageId: "exceed" }], + }, + { + code: "const foo = a => {};", + options: [{ max: 0 }], + errors: [{ messageId: "exceed" }], + }, + { + code: ` + class Foo { + method(this: void, a, b, c, d) {} + } + `, + errors: [{ messageId: "exceed" }], + }, + { + code: ` + class Foo { + method(this: void, a) {} + } + `, + options: [{ countVoidThis: true, max: 1 }], + errors: [{ messageId: "exceed" }], + }, + { + code: `function testD(this: void, a) {}`, + options: [{ countVoidThis: true, max: 1 }], + errors: [{ messageId: "exceed" }], + }, + { + code: `const testE = function (this: void, a) {}`, + options: [{ countVoidThis: true, max: 1 }], + errors: [{ messageId: "exceed" }], + }, + { + code: `function testFunction(test: void, a: number) {}`, + options: [{ countVoidThis: false, max: 1 }], + errors: [{ messageId: "exceed" }], + }, + { + code: ` + class Foo { + method(this: Foo, a, b, c) {} + } + `, + errors: [{ messageId: "exceed" }], + }, + { + code: ` + declare function makeDate(m: number, d: number, y: number): Date; + `, + options: [{ max: 1 }], + errors: [{ messageId: "exceed" }], + }, + { + code: ` + type sum = (a: number, b: number) => number; + `, + options: [{ max: 1 }], + errors: [{ messageId: "exceed" }], + }, + ], +}); From 44bac9d15c4e0ca099d0b0d85e601f3b55d4e167 Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Wed, 7 May 2025 00:27:08 +0200 Subject: [PATCH 08/36] ci: run tests in Node.js 24 (#19702) --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2bb85d7460df..7129baca809b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node: [23.x, 22.x, 21.x, 20.x, 18.x, "18.18.0"] + node: [24.x, 22.x, 20.x, 18.x, "18.18.0"] NODE_OPTIONS: [""] include: - os: windows-latest @@ -78,9 +78,9 @@ jobs: - os: macOS-latest node: "lts/*" - os: ubuntu-latest - node: 22.x + node: 24.x - # `--experimental-strip-types` is enabled by default in Node.js 23.x. + # `--experimental-strip-types` is enabled by default in Node.js 24.x. # This additional environment is necessary only to test `--experimental-transform-types`, # as it is not enabled by default in any Node.js version yet. NODE_OPTIONS: "--experimental-transform-types" From f0f0d46ab2f87e439642abd84b6948b447b66349 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 7 May 2025 07:33:09 +0200 Subject: [PATCH 09/36] docs: clarify that unused suppressions cause non-zero exit code (#19698) --- docs/src/use/suppressions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/use/suppressions.md b/docs/src/use/suppressions.md index d24d3aeee081..5b75cd3a8752 100644 --- a/docs/src/use/suppressions.md +++ b/docs/src/use/suppressions.md @@ -47,7 +47,7 @@ eslint --suppressions-location .github/.eslint-suppressions ## Resolving Suppressions -You can address any of the reported violations by making the necessary changes to the code as usual. If you run ESLint again you will notice that a warning is reported about unused suppressions. This is because the violations have been resolved but the suppressions are still in place. +You can address any of the reported violations by making the necessary changes to the code as usual. If you run ESLint again you will notice that it exits with a non-zero exit code and an error is reported about unused suppressions. This is because the violations have been resolved but the suppressions are still in place. ```bash > eslint From fbb8be9256dc7613fa0b87e87974714284b78a94 Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Wed, 7 May 2025 08:22:21 +0200 Subject: [PATCH 10/36] fix: add `info` to `ESLint.DeprecatedRuleUse` type (#19701) * fix: add `info` to `ESLint.DeprecatedRuleUse` type * fix typo --- lib/cli-engine/cli-engine.js | 2 +- lib/eslint/eslint.js | 3 ++- lib/eslint/legacy-eslint.js | 2 +- lib/types/index.d.ts | 16 ++++++++++++++++ tests/lib/types/types.test.ts | 14 ++++++++++++++ 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index ffa5c34c1153..176e6256a8d0 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -58,12 +58,12 @@ const validFixTypes = new Set(["directive", "problem", "suggestion", "layout"]); //------------------------------------------------------------------------------ // For VSCode IntelliSense -/** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ /** @typedef {import("../shared/types").LintMessage} LintMessage */ /** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */ /** @typedef {import("../shared/types").ParserOptions} ParserOptions */ /** @typedef {import("../shared/types").RuleConf} RuleConf */ /** @typedef {import("../types").ESLint.ConfigData} ConfigData */ +/** @typedef {import("../types").ESLint.DeprecatedRuleUse} DeprecatedRuleInfo */ /** @typedef {import("../types").ESLint.FormatterFunction} FormatterFunction */ /** @typedef {import("../types").ESLint.Plugin} Plugin */ /** @typedef {import("../types").Rule.RuleModule} Rule */ diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index d9e9348bc14d..a9203d44a52d 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -57,11 +57,12 @@ const { ConfigLoader, LegacyConfigLoader } = require("../config/config-loader"); * @import { CLIEngineLintReport } from "./legacy-eslint.js"; * @import { FlatConfigArray } from "../config/flat-config-array.js"; * @import { RuleDefinition } from "@eslint/core"; - * @import { DeprecatedRuleInfo, LintMessage, LintResult, ResultsMeta } from "../shared/types.js"; + * @import { LintMessage, LintResult, ResultsMeta } from "../shared/types.js"; */ /** @typedef {ReturnType} ExtractedConfig */ /** @typedef {import("../types").Linter.Config} Config */ +/** @typedef {import("../types").ESLint.DeprecatedRuleUse} DeprecatedRuleInfo */ /** @typedef {import("../types").ESLint.Plugin} Plugin */ /** diff --git a/lib/eslint/legacy-eslint.js b/lib/eslint/legacy-eslint.js index 72caa83a4fed..0f1778ec3ec0 100644 --- a/lib/eslint/legacy-eslint.js +++ b/lib/eslint/legacy-eslint.js @@ -30,12 +30,12 @@ const { version } = require("../../package.json"); //------------------------------------------------------------------------------ /** @typedef {import("../cli-engine/cli-engine").LintReport} CLIEngineLintReport */ -/** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ /** @typedef {import("../shared/types").LintMessage} LintMessage */ /** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */ /** @typedef {import("../shared/types").LintResult} LintResult */ /** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */ /** @typedef {import("../types").ESLint.ConfigData} ConfigData */ +/** @typedef {import("../types").ESLint.DeprecatedRuleUse} DeprecatedRuleInfo */ /** @typedef {import("../types").ESLint.Plugin} Plugin */ /** @typedef {import("../types").Rule.RuleModule} Rule */ diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts index f15b42e038f8..4a4fe218aeef 100644 --- a/lib/types/index.d.ts +++ b/lib/types/index.d.ts @@ -2057,9 +2057,25 @@ export namespace ESLint { }; } + /** + * Information about deprecated rules. + */ interface DeprecatedRuleUse { + /** + * The rule ID. + */ ruleId: string; + + /** + * The rule IDs that replace this deprecated rule. + */ replacedBy: string[]; + + /** + * The raw deprecated info provided by the rule. + * Unset if the rule's `meta.deprecated` property is a boolean. + */ + info?: DeprecatedInfo; } interface ResultsMeta { diff --git a/tests/lib/types/types.test.ts b/tests/lib/types/types.test.ts index 2c6ef87fd9de..6c61bf01de84 100644 --- a/tests/lib/types/types.test.ts +++ b/tests/lib/types/types.test.ts @@ -1774,6 +1774,20 @@ for (const result of results) { }; delete result.stats; + const deprecatedRule = result.usedDeprecatedRules[0]; + deprecatedRule.ruleId = "foo"; + deprecatedRule.replacedBy = ["bar"]; + deprecatedRule.info = { + message: "use bar instead", + replacedBy: [ + { + rule: { + name: "bar", + }, + }, + ], + }; + for (const message of result.messages) { message.ruleId = "foo"; } From ee4036429758cdaf7f77c52f1c2b74b5a2bb7b66 Mon Sep 17 00:00:00 2001 From: sethamus <32633697+sethamus@users.noreply.github.com> Date: Wed, 7 May 2025 16:08:39 +0300 Subject: [PATCH 11/36] feat: convert no-array-constructor suggestions to autofixes (#19621) * feat: convert no-array-constructor suggestions to autofixes * refactor * refactor * refactor * refactor * refactor * fix tests * revert format change * revert format change * sorry * refactor --- lib/rules/no-array-constructor.js | 52 ++- tests/lib/rules/no-array-constructor.js | 499 ++++++++++++++++++++---- 2 files changed, 469 insertions(+), 82 deletions(-) diff --git a/lib/rules/no-array-constructor.js b/lib/rules/no-array-constructor.js index e5044b09df9b..46e8f6b74863 100644 --- a/lib/rules/no-array-constructor.js +++ b/lib/rules/no-array-constructor.js @@ -34,6 +34,8 @@ module.exports = { url: "https://eslint.org/docs/latest/rules/no-array-constructor", }, + fixable: "code", + hasSuggestions: true, schema: [], @@ -49,6 +51,30 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; + /** + * Checks if there are comments in Array constructor expressions. + * @param {ASTNode} node A CallExpression or NewExpression node. + * @returns {boolean} True if there are comments, false otherwise. + */ + function hasCommentsInArrayConstructor(node) { + const firstToken = sourceCode.getFirstToken(node); + const lastToken = sourceCode.getLastToken(node); + + let lastRelevantToken = sourceCode.getLastToken(node.callee); + + while ( + lastRelevantToken !== lastToken && + !isOpeningParenToken(lastRelevantToken) + ) { + lastRelevantToken = sourceCode.getTokenAfter(lastRelevantToken); + } + + return sourceCode.commentsExistBetween( + firstToken, + lastRelevantToken, + ); + } + /** * Gets the text between the calling parentheses of a CallExpression or NewExpression. * @param {ASTNode} node A CallExpression or NewExpression node. @@ -107,6 +133,17 @@ module.exports = { let fixText; let messageId; + const nonSpreadCount = node.arguments.reduce( + (count, arg) => + arg.type !== "SpreadElement" ? count + 1 : count, + 0, + ); + + const shouldSuggest = + node.optional || + (node.arguments.length > 0 && nonSpreadCount < 2) || + hasCommentsInArrayConstructor(node); + /* * Check if the suggested change should include a preceding semicolon or not. * Due to JavaScript's ASI rules, a missing semicolon may be inserted automatically @@ -127,10 +164,23 @@ module.exports = { context.report({ node, messageId: "preferLiteral", + fix(fixer) { + if (shouldSuggest) { + return null; + } + + return fixer.replaceText(node, fixText); + }, suggest: [ { messageId, - fix: fixer => fixer.replaceText(node, fixText), + fix(fixer) { + if (shouldSuggest) { + return fixer.replaceText(node, fixText); + } + + return null; + }, }, ], }); diff --git a/tests/lib/rules/no-array-constructor.js b/tests/lib/rules/no-array-constructor.js index db0acc10eda3..1e3d5bb11f62 100644 --- a/tests/lib/rules/no-array-constructor.js +++ b/tests/lib/rules/no-array-constructor.js @@ -16,6 +16,17 @@ const rule = require("../../../lib/rules/no-array-constructor"), // Tests //------------------------------------------------------------------------------ +/** + * Removes any leading whitespace (spaces, tabs, etc.) that immediately + * follows a newline character within a string. + * @param {string} str The input string to process. + * @returns {string} A new string with leading whitespace removed from + * the beginning of each line (after the newline). + */ +function stripNewlineIndent(str) { + return str.replace(/(\n)\s+/gu, "$1"); +} + const ruleTester = new RuleTester({ languageOptions: { sourceType: "script", @@ -47,61 +58,41 @@ ruleTester.run("no-array-constructor", rule, { invalid: [ { code: "new Array()", + output: "[]", errors: [ { messageId: "preferLiteral", type: "NewExpression", - suggestions: [ - { - messageId: "useLiteral", - output: "[]", - }, - ], }, ], }, { code: "new Array", + output: "[]", errors: [ { messageId: "preferLiteral", type: "NewExpression", - suggestions: [ - { - messageId: "useLiteral", - output: "[]", - }, - ], }, ], }, { code: "new Array(x, y)", + output: "[x, y]", errors: [ { messageId: "preferLiteral", type: "NewExpression", - suggestions: [ - { - messageId: "useLiteral", - output: "[x, y]", - }, - ], }, ], }, { code: "new Array(0, 1, 2)", + output: "[0, 1, 2]", errors: [ { messageId: "preferLiteral", type: "NewExpression", - suggestions: [ - { - messageId: "useLiteral", - output: "[0, 1, 2]", - }, - ], }, ], }, @@ -127,6 +118,21 @@ ruleTester.run("no-array-constructor", rule, { b = c() // bar ); `, + output: ` + const array = [ + /* foo */ a, + b = c() // bar + ]; + `, + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + }, + ], + }, + { + code: "const array = Array(...args);", errors: [ { messageId: "preferLiteral", @@ -134,19 +140,14 @@ ruleTester.run("no-array-constructor", rule, { suggestions: [ { messageId: "useLiteral", - output: ` - const array = [ - /* foo */ a, - b = c() // bar - ]; - `, + output: "const array = [...args];", }, ], }, ], }, { - code: "const array = Array(...args);", + code: "const array = Array(...foo, ...bar);", errors: [ { messageId: "preferLiteral", @@ -154,14 +155,14 @@ ruleTester.run("no-array-constructor", rule, { suggestions: [ { messageId: "useLiteral", - output: "const array = [...args];", + output: "const array = [...foo, ...bar];", }, ], }, ], }, { - code: "a = new (Array);", + code: "const array = new Array(...args);", errors: [ { messageId: "preferLiteral", @@ -169,27 +170,57 @@ ruleTester.run("no-array-constructor", rule, { suggestions: [ { messageId: "useLiteral", - output: "a = [];", + output: "const array = [...args];", }, ], }, ], }, { - code: "a = new (Array) && (foo);", + code: "const array = Array(5, ...args);", errors: [ { messageId: "preferLiteral", - type: "NewExpression", + type: "CallExpression", suggestions: [ { messageId: "useLiteral", - output: "a = [] && (foo);", + output: "const array = [5, ...args];", }, ], }, ], }, + { + code: "const array = Array(5, 6, ...args);", + output: "const array = [5, 6, ...args];", + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + }, + ], + }, + { + code: "a = new (Array);", + output: "a = [];", + errors: [ + { + messageId: "preferLiteral", + type: "NewExpression", + }, + ], + }, + { + code: "a = new (Array) && (foo);", + output: "a = [] && (foo);", + errors: [ + { + messageId: "preferLiteral", + type: "NewExpression", + }, + ], + }, ...[ // Semicolon required before array literal to compensate for ASI @@ -273,18 +304,13 @@ ruleTester.run("no-array-constructor", rule, { }, ].map(props => ({ ...props, + output: props.code.replace( + /(new )?Array\((?.*?)\)/su, + ";[$]", + ), errors: [ { messageId: "preferLiteral", - suggestions: [ - { - messageId: "useLiteralAfterSemicolon", - output: props.code.replace( - /(new )?Array\((?.*?)\)/su, - ";[$]", - ), - }, - ], }, ], })), @@ -469,21 +495,330 @@ ruleTester.run("no-array-constructor", rule, { }, ].map(props => ({ ...props, + output: props.code.replace( + /(new )?Array\((?.*?)\)/su, + "[$]", + ), + errors: [ + { + messageId: "preferLiteral", + }, + ], + })), + { + code: "/*a*/Array()", + output: "/*a*/[]", + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + }, + ], + }, + { + code: "/*a*/Array()/*b*/", + output: "/*a*/[]/*b*/", + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + }, + ], + }, + { + code: "Array/*a*/()", errors: [ { messageId: "preferLiteral", + type: "CallExpression", suggestions: [ { messageId: "useLiteral", - output: props.code.replace( - /(new )?Array\((?.*?)\)/su, - "[$]", - ), + output: "[]", }, ], }, ], - })), + }, + { + code: "/*a*//*b*/Array/*c*//*d*/()/*e*//*f*/;/*g*//*h*/", + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + suggestions: [ + { + messageId: "useLiteral", + output: "/*a*//*b*/[]/*e*//*f*/;/*g*//*h*/", + }, + ], + }, + ], + }, + { + code: "Array(/*a*/ /*b*/)", + output: "[/*a*/ /*b*/]", + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + }, + ], + }, + { + code: "Array(/*a*/ x /*b*/, /*c*/ y /*d*/)", + output: "[/*a*/ x /*b*/, /*c*/ y /*d*/]", + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + }, + ], + }, + { + code: "/*a*/Array(/*b*/ x /*c*/, /*d*/ y /*e*/)/*f*/;/*g*/", + output: "/*a*/[/*b*/ x /*c*/, /*d*/ y /*e*/]/*f*/;/*g*/", + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + }, + ], + }, + { + code: "/*a*/new Array", + output: "/*a*/[]", + errors: [ + { + messageId: "preferLiteral", + type: "NewExpression", + }, + ], + }, + { + code: "/*a*/new Array/*b*/", + output: "/*a*/[]/*b*/", + errors: [ + { + messageId: "preferLiteral", + type: "NewExpression", + }, + ], + }, + { + code: "new/*a*/Array", + errors: [ + { + messageId: "preferLiteral", + type: "NewExpression", + suggestions: [ + { + messageId: "useLiteral", + output: "[]", + }, + ], + }, + ], + }, + { + code: "new/*a*//*b*/Array/*c*//*d*/()/*e*//*f*/;/*g*//*h*/", + errors: [ + { + messageId: "preferLiteral", + type: "NewExpression", + suggestions: [ + { + messageId: "useLiteral", + output: "[]/*e*//*f*/;/*g*//*h*/", + }, + ], + }, + ], + }, + { + code: "new Array(/*a*/ /*b*/)", + output: "[/*a*/ /*b*/]", + errors: [ + { + messageId: "preferLiteral", + type: "NewExpression", + }, + ], + }, + { + code: "new Array(/*a*/ x /*b*/, /*c*/ y /*d*/)", + output: "[/*a*/ x /*b*/, /*c*/ y /*d*/]", + errors: [ + { + messageId: "preferLiteral", + type: "NewExpression", + }, + ], + }, + { + code: "new/*a*/Array(/*b*/ x /*c*/, /*d*/ y /*e*/)/*f*/;/*g*/", + errors: [ + { + messageId: "preferLiteral", + type: "NewExpression", + suggestions: [ + { + messageId: "useLiteral", + output: "[/*b*/ x /*c*/, /*d*/ y /*e*/]/*f*/;/*g*/", + }, + ], + }, + ], + }, + { + code: stripNewlineIndent(` + // a + Array // b + ()`), + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + suggestions: [ + { + messageId: "useLiteral", + output: stripNewlineIndent(` + // a + []`), + }, + ], + }, + ], + }, + { + code: stripNewlineIndent(` + // a + Array // b + () // c`), + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + suggestions: [ + { + messageId: "useLiteral", + output: stripNewlineIndent(` + // a + [] // c`), + }, + ], + }, + ], + }, + { + code: stripNewlineIndent(` + new // a + Array // b + ()`), + errors: [ + { + messageId: "preferLiteral", + type: "NewExpression", + suggestions: [ + { + messageId: "useLiteral", + output: stripNewlineIndent(` + []`), + }, + ], + }, + ], + }, + { + code: "new (Array /* a */);", + errors: [ + { + messageId: "preferLiteral", + type: "NewExpression", + suggestions: [ + { + messageId: "useLiteral", + output: "[];", + }, + ], + }, + ], + }, + { + code: "(/* a */ Array)(1, 2, 3);", + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + suggestions: [ + { + messageId: "useLiteral", + output: "[1, 2, 3];", + }, + ], + }, + ], + }, + { + code: "(Array /* a */)(1, 2, 3);", + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + suggestions: [ + { + messageId: "useLiteral", + output: "[1, 2, 3];", + }, + ], + }, + ], + }, + { + code: "(Array) /* a */ (1, 2, 3);", + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + suggestions: [ + { + messageId: "useLiteral", + output: "[1, 2, 3];", + }, + ], + }, + ], + }, + { + code: "(/* a */(Array))();", + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + suggestions: [ + { + messageId: "useLiteral", + output: "[];", + }, + ], + }, + ], + }, + { + code: "Array?.(0, 1, 2).forEach(doSomething);", + errors: [ + { + messageId: "preferLiteral", + type: "CallExpression", + suggestions: [ + { + messageId: "useLiteral", + output: "[0, 1, 2].forEach(doSomething);", + }, + ], + }, + ], + }, ], }); @@ -525,65 +860,64 @@ ruleTesterTypeScript.run("no-array-constructor", rule, { invalid: [ { code: "new Array();", + output: "[];", errors: [ { messageId: "preferLiteral", - suggestions: [ - { - messageId: "useLiteral", - output: "[];", - }, - ], }, ], }, { code: "Array();", + output: "[];", errors: [ { messageId: "preferLiteral", - suggestions: [ - { - messageId: "useLiteral", - output: "[];", - }, - ], }, ], }, { code: "new Array(x, y);", + output: "[x, y];", errors: [ { messageId: "preferLiteral", - suggestions: [ - { - messageId: "useLiteral", - output: "[x, y];", - }, - ], }, ], }, { code: "Array(x, y);", + output: "[x, y];", errors: [ { messageId: "preferLiteral", - suggestions: [ - { - messageId: "useLiteral", - output: "[x, y];", - }, - ], }, ], }, { code: "new Array(0, 1, 2);", + output: "[0, 1, 2];", + errors: [ + { + messageId: "preferLiteral", + }, + ], + }, + { + code: "Array(0, 1, 2);", + output: "[0, 1, 2];", + errors: [ + { + messageId: "preferLiteral", + }, + ], + }, + { + code: "Array?.(0, 1, 2);", errors: [ { messageId: "preferLiteral", + type: "CallExpression", suggestions: [ { messageId: "useLiteral", @@ -594,42 +928,45 @@ ruleTesterTypeScript.run("no-array-constructor", rule, { ], }, { - code: "Array(0, 1, 2);", + code: "Array?.(x, y);", errors: [ { messageId: "preferLiteral", + type: "CallExpression", suggestions: [ { messageId: "useLiteral", - output: "[0, 1, 2];", + output: "[x, y];", }, ], }, ], }, { - code: "Array?.(0, 1, 2);", + code: "Array /*a*/ ?.();", errors: [ { messageId: "preferLiteral", + type: "CallExpression", suggestions: [ { messageId: "useLiteral", - output: "[0, 1, 2];", + output: "[];", }, ], }, ], }, { - code: "Array?.(x, y);", + code: "Array?./*a*/();", errors: [ { messageId: "preferLiteral", + type: "CallExpression", suggestions: [ { messageId: "useLiteral", - output: "[x, y];", + output: "[];", }, ], }, From 7bc6c71ca350fa37531291e1d704be6ed408c5dc Mon Sep 17 00:00:00 2001 From: Jacob Bandes-Storch Date: Wed, 7 May 2025 07:31:07 -0700 Subject: [PATCH 12/36] feat: add no-unassigned-vars rule (#19618) * feat: add no-unassigned-vars rule * Apply suggestions from code review Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> Co-authored-by: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> * add typescript examples * Update docs/src/rules/no-unassigned-vars.md Co-authored-by: Nitin Kumar * review feedback * more review feedback * Apply suggestions from code review Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * npm run build:site * fix missing directive * review feedback * review feedback * add no-unused-vars to related_rules and add back-links from all related rules * review feedback * add When Not To Use It per @snitin315 feedback --------- Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> Co-authored-by: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> Co-authored-by: Nitin Kumar --- docs/src/_data/rules.json | 8 ++ docs/src/_data/rules_meta.json | 13 ++ docs/src/rules/init-declarations.md | 2 + docs/src/rules/no-unassigned-vars.md | 141 ++++++++++++++++++++ docs/src/rules/no-unused-vars.md | 1 + docs/src/rules/prefer-const.md | 1 + lib/rules/index.js | 1 + lib/rules/no-unassigned-vars.js | 72 +++++++++++ lib/types/rules.d.ts | 7 + packages/eslint-config-eslint/base.js | 1 + packages/js/src/configs/eslint-all.js | 1 + tests/lib/rules/no-unassigned-vars.js | 179 ++++++++++++++++++++++++++ 12 files changed, 427 insertions(+) create mode 100644 docs/src/rules/no-unassigned-vars.md create mode 100644 lib/rules/no-unassigned-vars.js create mode 100644 tests/lib/rules/no-unassigned-vars.js diff --git a/docs/src/_data/rules.json b/docs/src/_data/rules.json index c3f68d59278c..47a19b103c00 100644 --- a/docs/src/_data/rules.json +++ b/docs/src/_data/rules.json @@ -337,6 +337,14 @@ "frozen": false, "hasSuggestions": false }, + { + "name": "no-unassigned-vars", + "description": "Disallow `let` or `var` variables that are read but never assigned", + "recommended": false, + "fixable": false, + "frozen": false, + "hasSuggestions": false + }, { "name": "no-undef", "description": "Disallow the use of undeclared variables unless mentioned in `/*global */` comments", diff --git a/docs/src/_data/rules_meta.json b/docs/src/_data/rules_meta.json index 9abf524e8166..ef11109190c2 100644 --- a/docs/src/_data/rules_meta.json +++ b/docs/src/_data/rules_meta.json @@ -3260,6 +3260,19 @@ }, "fixable": "whitespace" }, + "no-unassigned-vars": { + "type": "problem", + "dialects": [ + "typescript", + "javascript" + ], + "language": "javascript", + "docs": { + "description": "Disallow `let` or `var` variables that are read but never assigned", + "recommended": false, + "url": "https://eslint.org/docs/latest/rules/no-unassigned-vars" + } + }, "no-undef": { "type": "problem", "defaultOptions": [ diff --git a/docs/src/rules/init-declarations.md b/docs/src/rules/init-declarations.md index a6d9f4e6e568..846dd7075a7e 100644 --- a/docs/src/rules/init-declarations.md +++ b/docs/src/rules/init-declarations.md @@ -1,6 +1,8 @@ --- title: init-declarations rule_type: suggestion +related_rules: +- no-unassigned-vars --- diff --git a/docs/src/rules/no-unassigned-vars.md b/docs/src/rules/no-unassigned-vars.md new file mode 100644 index 000000000000..a2ae0ba393b7 --- /dev/null +++ b/docs/src/rules/no-unassigned-vars.md @@ -0,0 +1,141 @@ +--- +title: no-unassigned-vars +rule_type: problem +related_rules: +- init-declarations +- no-unused-vars +- prefer-const +--- + + +This rule flags `let` or `var` declarations that are never assigned a value but are still read or used in the code. Since these variables will always be `undefined`, their usage is likely a programming mistake. + +For example, if you check the value of a `status` variable, but it was never given a value, it will always be `undefined`: + +```js +let status; + +// ...forgot to assign a value to status... + +if (status === 'ready') { + console.log('Ready!'); +} +``` + +## Rule Details + +Examples of **incorrect** code for this rule: + +::: incorrect + +```js +/*eslint no-unassigned-vars: "error"*/ + +let status; +if (status === 'ready') { + console.log('Ready!'); +} + +let user; +greet(user); + +function test() { + let error; + return error || "Unknown error"; +} + +let options; +const { debug } = options || {}; + +let flag; +while (!flag) { + // Do something... +} + +let config; +function init() { + return config?.enabled; +} +``` + +::: + +In TypeScript: + +::: incorrect + +```ts +/*eslint no-unassigned-vars: "error"*/ + +let value: number | undefined; +console.log(value); +``` + +::: + +Examples of **correct** code for this rule: + +::: correct + +```js +/*eslint no-unassigned-vars: "error"*/ + +let message = "hello"; +console.log(message); + +let user; +user = getUser(); +console.log(user.name); + +let count; +count = 1; +count++; + +// Variable is unused (should be reported by `no-unused-vars` only) +let temp; + +let error; +if (somethingWentWrong) { + error = "Something went wrong"; +} +console.log(error); + +let item; +for (item of items) { + process(item); +} + +let config; +function setup() { + config = { debug: true }; +} +setup(); +console.log(config); + +let one = undefined; +if (one === two) { + // Noop +} +``` + +::: + +In TypeScript: + +::: correct + +```ts +/*eslint no-unassigned-vars: "error"*/ + +declare let value: number | undefined; +console.log(value); +``` + +::: + +## When Not To Use It + +You can disable this rule if your code intentionally uses variables that are declared and used, but are never assigned a value. This might be the case in: + +- Legacy codebases where uninitialized variables are used as placeholders. +- Certain TypeScript use cases where variables are declared with a type and intentionally left unassigned (though using `declare` is preferred). diff --git a/docs/src/rules/no-unused-vars.md b/docs/src/rules/no-unused-vars.md index 96e70e6becc0..872a888bed50 100644 --- a/docs/src/rules/no-unused-vars.md +++ b/docs/src/rules/no-unused-vars.md @@ -2,6 +2,7 @@ title: no-unused-vars rule_type: problem related_rules: +- no-unassigned-vars - no-useless-assignment --- diff --git a/docs/src/rules/prefer-const.md b/docs/src/rules/prefer-const.md index b61b5e6ae73d..449a41b82a6e 100644 --- a/docs/src/rules/prefer-const.md +++ b/docs/src/rules/prefer-const.md @@ -3,6 +3,7 @@ title: prefer-const rule_type: suggestion related_rules: - no-var +- no-unassigned-vars - no-use-before-define --- diff --git a/lib/rules/index.js b/lib/rules/index.js index be0e51619bc5..0034ac0520b1 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -225,6 +225,7 @@ module.exports = new LazyLoadingRuleMap( "no-this-before-super": () => require("./no-this-before-super"), "no-throw-literal": () => require("./no-throw-literal"), "no-trailing-spaces": () => require("./no-trailing-spaces"), + "no-unassigned-vars": () => require("./no-unassigned-vars"), "no-undef": () => require("./no-undef"), "no-undef-init": () => require("./no-undef-init"), "no-undefined": () => require("./no-undefined"), diff --git a/lib/rules/no-unassigned-vars.js b/lib/rules/no-unassigned-vars.js new file mode 100644 index 000000000000..0518eb41dcb3 --- /dev/null +++ b/lib/rules/no-unassigned-vars.js @@ -0,0 +1,72 @@ +/** + * @fileoverview Rule to flag variables that are never assigned + * @author Jacob Bandes-Storch + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/** @type {import('../types').Rule.RuleModule} */ +module.exports = { + meta: { + type: "problem", + dialects: ["typescript", "javascript"], + language: "javascript", + + docs: { + description: + "Disallow `let` or `var` variables that are read but never assigned", + recommended: false, + url: "https://eslint.org/docs/latest/rules/no-unassigned-vars", + }, + + schema: [], + messages: { + unassigned: + "'{{name}}' is always 'undefined' because it's never assigned.", + }, + }, + + create(context) { + const sourceCode = context.sourceCode; + + return { + VariableDeclarator(node) { + /** @type {import('estree').VariableDeclaration} */ + const declaration = node.parent; + const shouldCheck = + !node.init && + node.id.type === "Identifier" && + declaration.kind !== "const" && + !declaration.declare; + if (!shouldCheck) { + return; + } + const [variable] = sourceCode.getDeclaredVariables(node); + if (!variable) { + return; + } + let hasRead = false; + for (const reference of variable.references) { + if (reference.isWrite()) { + return; + } + if (reference.isRead()) { + hasRead = true; + } + } + if (!hasRead) { + // Variables that are never read should be flagged by no-unused-vars instead + return; + } + context.report({ + node, + messageId: "unassigned", + data: { name: node.id.name }, + }); + }, + }; + }, +}; diff --git a/lib/types/rules.d.ts b/lib/types/rules.d.ts index 91524a6cb536..0a5d1f0bfdb9 100644 --- a/lib/types/rules.d.ts +++ b/lib/types/rules.d.ts @@ -3714,6 +3714,13 @@ export interface ESLintRules extends Linter.RulesRecord { ] >; + /** + * Rule to disallow `let` or `var` variables that are read but never assigned. + * + * @see https://eslint.org/docs/latest/rules/no-unassigned-vars + */ + "no-unassigned-vars": Linter.RuleEntry<[]>; + /** * Rule to disallow the use of undeclared variables unless mentioned in \/*global *\/ comments. * diff --git a/packages/eslint-config-eslint/base.js b/packages/eslint-config-eslint/base.js index f2f4580e78fd..bd0cbd59d3b4 100644 --- a/packages/eslint-config-eslint/base.js +++ b/packages/eslint-config-eslint/base.js @@ -88,6 +88,7 @@ const jsConfigs = [ "no-sequences": "error", "no-shadow": "error", "no-throw-literal": "error", + "no-unassigned-vars": "error", "no-undef": ["error", { typeof: true }], "no-undef-init": "error", "no-undefined": "error", diff --git a/packages/js/src/configs/eslint-all.js b/packages/js/src/configs/eslint-all.js index a854e5f16ce5..aee729eb039b 100644 --- a/packages/js/src/configs/eslint-all.js +++ b/packages/js/src/configs/eslint-all.js @@ -149,6 +149,7 @@ module.exports = Object.freeze({ "no-ternary": "error", "no-this-before-super": "error", "no-throw-literal": "error", + "no-unassigned-vars": "error", "no-undef": "error", "no-undef-init": "error", "no-undefined": "error", diff --git a/tests/lib/rules/no-unassigned-vars.js b/tests/lib/rules/no-unassigned-vars.js new file mode 100644 index 000000000000..7ed3b9d6955e --- /dev/null +++ b/tests/lib/rules/no-unassigned-vars.js @@ -0,0 +1,179 @@ +/** + * @fileoverview Tests for no-unassigned-vars rule. + * @author Jacob Bandes-Storch + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/no-unassigned-vars"), + RuleTester = require("../../../lib/rule-tester/rule-tester"); +const { unIndent } = require("../../_utils"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: "script", + }, +}); + +ruleTester.run("no-unassigned-vars", rule, { + valid: [ + "let x;", + "var x;", + "const x = undefined; log(x);", + "let y = undefined; log(y);", + "var y = undefined; log(y);", + "let a = x, b = y; log(a, b);", + "var a = x, b = y; log(a, b);", + "const foo = (two) => { let one; if (one !== two) one = two; }", + ], + invalid: [ + { + code: "let x; let a = x, b; log(x, a, b);", + errors: [ + { + messageId: "unassigned", + line: 1, + column: 5, + data: { name: "x" }, + }, + { + messageId: "unassigned", + line: 1, + column: 19, + data: { name: "b" }, + }, + ], + }, + { + code: "const foo = (two) => { let one; if (one === two) {} }", + errors: [ + { + messageId: "unassigned", + line: 1, + column: 28, + data: { name: "one" }, + }, + ], + }, + { + code: "let user; greet(user);", + errors: [ + { + messageId: "unassigned", + line: 1, + column: 5, + data: { name: "user" }, + }, + ], + }, + { + code: "function test() { let error; return error || 'Unknown error'; }", + errors: [ + { + messageId: "unassigned", + line: 1, + column: 23, + data: { name: "error" }, + }, + ], + }, + { + code: "let options; const { debug } = options || {};", + errors: [ + { + messageId: "unassigned", + line: 1, + column: 5, + data: { name: "options" }, + }, + ], + }, + { + code: "let flag; while (!flag) { }", + errors: [ + { + messageId: "unassigned", + line: 1, + column: 5, + data: { name: "flag" }, + }, + ], + }, + { + code: "let config; function init() { return config?.enabled; }", + errors: [ + { + messageId: "unassigned", + line: 1, + column: 5, + data: { name: "config" }, + }, + ], + }, + ], +}); + +const ruleTesterTypeScript = new RuleTester({ + languageOptions: { + parser: require("@typescript-eslint/parser"), + }, +}); + +ruleTesterTypeScript.run("no-unassigned-vars", rule, { + valid: [ + "let z: number | undefined = undefined; log(z);", + "declare let c: string | undefined; log(c);", + unIndent` + const foo = (two: string): void => { + let one: string | undefined; + if (one !== two) { + one = two; + } + } + `, + ], + invalid: [ + { + code: "let x: number; log(x);", + errors: [ + { + messageId: "unassigned", + line: 1, + column: 5, + data: { name: "x" }, + }, + ], + }, + { + code: "let x: number | undefined; log(x);", + errors: [ + { + messageId: "unassigned", + line: 1, + column: 5, + data: { name: "x" }, + }, + ], + }, + { + code: "const foo = (two: string): void => { let one: string | undefined; if (one === two) {} }", + errors: [ + { + messageId: "unassigned", + line: 1, + column: 42, + data: { name: "one" }, + }, + ], + }, + ], +}); From 9da90ca3c163adb23a9cc52421f59dedfce34fc9 Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Thu, 8 May 2025 07:27:07 +0200 Subject: [PATCH 13/36] fix: add `allowReserved` to `Linter.ParserOptions` type (#19710) --- lib/cli-engine/cli-engine.js | 2 +- lib/linter/linter.js | 2 +- lib/types/index.d.ts | 9 ++++++++- tests/lib/types/types.test.ts | 20 ++++++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 176e6256a8d0..dbaf9d3e1262 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -60,11 +60,11 @@ const validFixTypes = new Set(["directive", "problem", "suggestion", "layout"]); // For VSCode IntelliSense /** @typedef {import("../shared/types").LintMessage} LintMessage */ /** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */ -/** @typedef {import("../shared/types").ParserOptions} ParserOptions */ /** @typedef {import("../shared/types").RuleConf} RuleConf */ /** @typedef {import("../types").ESLint.ConfigData} ConfigData */ /** @typedef {import("../types").ESLint.DeprecatedRuleUse} DeprecatedRuleInfo */ /** @typedef {import("../types").ESLint.FormatterFunction} FormatterFunction */ +/** @typedef {import("../types").Linter.ParserOptions} ParserOptions */ /** @typedef {import("../types").ESLint.Plugin} Plugin */ /** @typedef {import("../types").Rule.RuleModule} Rule */ /** @typedef {ReturnType} ConfigArray */ diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 85982616b583..34bbe64ef728 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -79,12 +79,12 @@ const STEP_KIND_CALL = 2; /** @typedef {import("../shared/types").GlobalConf} GlobalConf */ /** @typedef {import("../shared/types").LintMessage} LintMessage */ /** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */ -/** @typedef {import("../shared/types").ParserOptions} ParserOptions */ /** @typedef {import("../shared/types").Processor} Processor */ /** @typedef {import("../shared/types").Times} Times */ /** @typedef {import("../types").Linter.Config} Config */ /** @typedef {import("../types").ESLint.ConfigData} ConfigData */ /** @typedef {import("../types").Linter.LanguageOptions} JSLanguageOptions */ +/** @typedef {import("../types").Linter.ParserOptions} ParserOptions */ /** @typedef {import("../types").Linter.StringSeverity} StringSeverity */ /** @typedef {import("../types").Rule.RuleModule} Rule */ diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts index 4a4fe218aeef..8c55d8a63f13 100644 --- a/lib/types/index.d.ts +++ b/lib/types/index.d.ts @@ -1560,9 +1560,16 @@ export namespace Linter { /** * Parser options. * - * @see [Specifying Parser Options](https://eslint.org/docs/latest/use/configure/language-options-deprecated#specifying-parser-options) + * @see [Specifying Parser Options](https://eslint.org/docs/latest/use/configure/language-options#specifying-parser-options) */ interface ParserOptions { + /** + * Allow the use of reserved words as identifiers (if `ecmaVersion` is 3). + * + * @default false + */ + allowReserved?: boolean | undefined; + /** * Accepts any valid ECMAScript version number or `'latest'`: * diff --git a/tests/lib/types/types.test.ts b/tests/lib/types/types.test.ts index 6c61bf01de84..db6b4f52f32c 100644 --- a/tests/lib/types/types.test.ts +++ b/tests/lib/types/types.test.ts @@ -945,6 +945,16 @@ linter.verify( }, "test.js", ); +linter.verify( + SOURCE, + { + parserOptions: { + ecmaVersion: 3, + allowReserved: true, + }, + }, + "test.js", +); linter.verify(SOURCE, { env: { node: true } }, "test.js"); linter.verify(SOURCE, { globals: { foo: true } }, "test.js"); linter.verify(SOURCE, { globals: { foo: "off" } }, "test.js"); @@ -1340,6 +1350,16 @@ linterWithEslintrcConfig.verify( }, "test.js", ); +linterWithEslintrcConfig.verify( + SOURCE, + { + parserOptions: { + ecmaVersion: 3, + allowReserved: true, + }, + }, + "test.js", +); linterWithEslintrcConfig.verify(SOURCE, { env: { node: true } }, "test.js"); linterWithEslintrcConfig.verify(SOURCE, { globals: { foo: true } }, "test.js"); linterWithEslintrcConfig.verify(SOURCE, { globals: { foo: "off" } }, "test.js"); From 4c289e685e6cf87331f4b1e6afe34a4feb8e6cc8 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Thu, 8 May 2025 08:09:51 +0000 Subject: [PATCH 14/36] docs: Update README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 595402b06b47..6184974d6188 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,11 @@ Josh Goldberg ✨ Tanuj Kanti's Avatar
Tanuj Kanti + + +루밀LuMir's Avatar
+루밀LuMir +
### Website Team From 60c3e2cf9256f3676b7934e26ff178aaf19c9e97 Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Fri, 9 May 2025 02:20:26 +1000 Subject: [PATCH 15/36] fix: sort keys in eslint-suppressions.json to avoid git churn (#19711) fix: sort keys in eslint-suppressions.json to avoid `git` churn/conflicts --- lib/services/suppressions-service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/services/suppressions-service.js b/lib/services/suppressions-service.js index 47fe84c8dab7..e72cd0da1e59 100644 --- a/lib/services/suppressions-service.js +++ b/lib/services/suppressions-service.js @@ -12,6 +12,7 @@ const fs = require("node:fs"); const path = require("node:path"); const { calculateStatsPerFile } = require("../eslint/eslint-helpers"); +const stringify = require("json-stable-stringify-without-jsonify"); //------------------------------------------------------------------------------ // Typedefs @@ -224,7 +225,7 @@ class SuppressionsService { save(suppressions) { return fs.promises.writeFile( this.filePath, - JSON.stringify(suppressions, null, 2), + stringify(suppressions, { space: 2 }), ); } From 3a075a29cfb43ef08711c2e433fb6f218855886d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 10:58:29 +0200 Subject: [PATCH 16/36] chore: update dependency @eslint/core to ^0.14.0 (#19715) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 99e86a281d86..6195104cf594 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.26.0", "@eslint/plugin-kit": "^0.2.8", From 58a171e8f0dcc1e599ac22bf8c386abacdbee424 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 11:31:00 +0200 Subject: [PATCH 17/36] chore: update dependency @eslint/plugin-kit to ^0.3.1 (#19712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update dependency @eslint/plugin-kit to ^0.3.0 * wip: update package.json --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: 루밀LuMir --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6195104cf594..1509eaa597f5 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.26.0", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", From de1b5deba069f770140f3a7dba2702c1016dcc2a Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Fri, 9 May 2025 13:14:38 +0200 Subject: [PATCH 18/36] fix: correct `service` property name in `Linter.ESLintParseResult` type (#19713) --- lib/types/index.d.ts | 13 ++++++++++--- tests/lib/types/types.test.ts | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts index 8c55d8a63f13..1328b2960c87 100644 --- a/lib/types/index.d.ts +++ b/lib/types/index.d.ts @@ -1668,8 +1668,8 @@ export namespace Linter { messages: LintMessage[]; } - // Temporarily loosen type for just flat config files (see #68232) - type NonESTreeParser = Omit & + // Temporarily loosen type for just flat config files (see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68232) + type NonESTreeParser = ESLint.ObjectMetaProperties & ( | { parse(text: string, options?: any): unknown; @@ -1694,9 +1694,16 @@ export namespace Linter { type Parser = NonESTreeParser | ESTreeParser; interface ESLintParseResult { + /** The AST object. */ ast: AST.Program; - parserServices?: SourceCode.ParserServices | undefined; + + /** The services that the parser provides. */ + services?: SourceCode.ParserServices | undefined; + + /** The scope manager of the AST. */ scopeManager?: Scope.ScopeManager | undefined; + + /** The visitor keys of the AST. */ visitorKeys?: SourceCode.VisitorKeys | undefined; } diff --git a/tests/lib/types/types.test.ts b/tests/lib/types/types.test.ts index db6b4f52f32c..30ffb11c4840 100644 --- a/tests/lib/types/types.test.ts +++ b/tests/lib/types/types.test.ts @@ -1083,11 +1083,11 @@ linter.defineParser("custom-parser", { name: "foo", version: "1.2.3", }, - parseForESLint(src, opts) { + parseForESLint(src, opts): Linter.ESLintParseResult { return { ast: AST, visitorKeys: {}, - parserServices: {}, + services: {}, scopeManager, }; }, From 71317ebeaf1c542114e4fcda99ee26115d8e4a27 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Sat, 10 May 2025 08:08:21 +0000 Subject: [PATCH 19/36] docs: Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6184974d6188..70b434d31574 100644 --- a/README.md +++ b/README.md @@ -325,7 +325,7 @@ The following companies, organizations, and individuals support ESLint's ongoing to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Diamond Sponsors

-

AG Grid

Platinum Sponsors

+

AG Grid

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

Qlty Software trunk.io Shopify

Silver Sponsors

Vite Liftoff American Express StackBlitz

Bronze Sponsors

From f60f2764971a33e252be13e560dccf21f554dbf1 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Sat, 10 May 2025 12:52:45 -0400 Subject: [PATCH 20/36] refactor: Easier RuleContext creation (#19709) refs #18787 --- lib/linter/file-context.js | 11 ++ lib/linter/linter.js | 102 +++++++++--------- tests/lib/linter/file-context.js | 177 +++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 52 deletions(-) create mode 100644 tests/lib/linter/file-context.js diff --git a/lib/linter/file-context.js b/lib/linter/file-context.js index d8909454840f..40acdd63cd26 100644 --- a/lib/linter/file-context.js +++ b/lib/linter/file-context.js @@ -128,6 +128,17 @@ class FileContext { getSourceCode() { return this.sourceCode; } + + /** + * Creates a new object with the current object as the prototype and + * the specified properties as its own properties. + * @param {Object} extension The properties to add to the new object. + * @returns {FileContext} A new object with the current object as the prototype + * and the specified properties as its own properties. + */ + extend(extension) { + return Object.freeze(Object.assign(Object.create(this), extension)); + } } exports.FileContext = FileContext; diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 34bbe64ef728..7c04792e5ceb 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -1185,7 +1185,7 @@ function runRules( * All rule contexts will inherit from this object. This avoids the performance penalty of copying all the * properties once for each rule. */ - const sharedTraversalContext = new FileContext({ + const fileContext = new FileContext({ cwd, filename, physicalFilename: physicalFilename || filename, @@ -1221,63 +1221,61 @@ function runRules( const messageIds = rule.meta && rule.meta.messages; let reportTranslator = null; - const ruleContext = Object.freeze( - Object.assign(Object.create(sharedTraversalContext), { - id: ruleId, - options: getRuleOptions( - configuredRules[ruleId], - applyDefaultOptions ? rule.meta?.defaultOptions : void 0, - ), - report(...args) { - /* - * Create a report translator lazily. - * In a vast majority of cases, any given rule reports zero errors on a given - * piece of code. Creating a translator lazily avoids the performance cost of - * creating a new translator function for each rule that usually doesn't get - * called. - * - * Using lazy report translators improves end-to-end performance by about 3% - * with Node 8.4.0. - */ - if (reportTranslator === null) { - reportTranslator = createReportTranslator({ - ruleId, - severity, - sourceCode, - messageIds, - disableFixes, - language, - }); - } - const problem = reportTranslator(...args); + const ruleContext = fileContext.extend({ + id: ruleId, + options: getRuleOptions( + configuredRules[ruleId], + applyDefaultOptions ? rule.meta?.defaultOptions : void 0, + ), + report(...args) { + /* + * Create a report translator lazily. + * In a vast majority of cases, any given rule reports zero errors on a given + * piece of code. Creating a translator lazily avoids the performance cost of + * creating a new translator function for each rule that usually doesn't get + * called. + * + * Using lazy report translators improves end-to-end performance by about 3% + * with Node 8.4.0. + */ + if (reportTranslator === null) { + reportTranslator = createReportTranslator({ + ruleId, + severity, + sourceCode, + messageIds, + disableFixes, + language, + }); + } + const problem = reportTranslator(...args); - if (problem.fix && !(rule.meta && rule.meta.fixable)) { - throw new Error( - 'Fixable rules must set the `meta.fixable` property to "code" or "whitespace".', - ); - } + if (problem.fix && !(rule.meta && rule.meta.fixable)) { + throw new Error( + 'Fixable rules must set the `meta.fixable` property to "code" or "whitespace".', + ); + } + if ( + problem.suggestions && + !(rule.meta && rule.meta.hasSuggestions === true) + ) { if ( - problem.suggestions && - !(rule.meta && rule.meta.hasSuggestions === true) + rule.meta && + rule.meta.docs && + typeof rule.meta.docs.suggestion !== "undefined" ) { - if ( - rule.meta && - rule.meta.docs && - typeof rule.meta.docs.suggestion !== "undefined" - ) { - // Encourage migration from the former property name. - throw new Error( - "Rules with suggestions must set the `meta.hasSuggestions` property to `true`. `meta.docs.suggestion` is ignored by ESLint.", - ); - } + // Encourage migration from the former property name. throw new Error( - "Rules with suggestions must set the `meta.hasSuggestions` property to `true`.", + "Rules with suggestions must set the `meta.hasSuggestions` property to `true`. `meta.docs.suggestion` is ignored by ESLint.", ); } - lintingProblems.push(problem); - }, - }), - ); + throw new Error( + "Rules with suggestions must set the `meta.hasSuggestions` property to `true`.", + ); + } + lintingProblems.push(problem); + }, + }); const ruleListenersReturn = timing.enabled || stats diff --git a/tests/lib/linter/file-context.js b/tests/lib/linter/file-context.js new file mode 100644 index 000000000000..1b9469636e00 --- /dev/null +++ b/tests/lib/linter/file-context.js @@ -0,0 +1,177 @@ +/** + * @fileoverview Tests for FileContext class. + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const assert = require("chai").assert; +const { FileContext } = require("../../../lib/linter/file-context"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe("FileContext", () => { + const mockSourceCode = {}; + const defaultConfig = { + cwd: "/path/to/project", + filename: "test.js", + physicalFilename: "/path/to/project/test.js", + sourceCode: mockSourceCode, + parserOptions: { ecmaVersion: 2021 }, + parserPath: "/path/to/parser", + languageOptions: { ecmaVersion: 2022 }, + settings: { env: { es6: true } }, + }; + + describe("constructor", () => { + it("should create a frozen instance with all properties set", () => { + const context = new FileContext(defaultConfig); + + assert.strictEqual(context.cwd, defaultConfig.cwd); + assert.strictEqual(context.filename, defaultConfig.filename); + assert.strictEqual( + context.physicalFilename, + defaultConfig.physicalFilename, + ); + assert.strictEqual(context.sourceCode, defaultConfig.sourceCode); + assert.deepStrictEqual( + context.parserOptions, + defaultConfig.parserOptions, + ); + assert.strictEqual(context.parserPath, defaultConfig.parserPath); + assert.deepStrictEqual( + context.languageOptions, + defaultConfig.languageOptions, + ); + assert.deepStrictEqual(context.settings, defaultConfig.settings); + + // Verify the instance is frozen + assert.throws(() => { + context.cwd = "changed"; + }, TypeError); + }); + + it("should allow partial configuration", () => { + const partialConfig = { + cwd: "/path/to/project", + filename: "test.js", + physicalFilename: "/path/to/project/test.js", + sourceCode: mockSourceCode, + }; + + const context = new FileContext(partialConfig); + + assert.strictEqual(context.cwd, partialConfig.cwd); + assert.strictEqual(context.filename, partialConfig.filename); + assert.strictEqual( + context.physicalFilename, + partialConfig.physicalFilename, + ); + assert.strictEqual(context.sourceCode, partialConfig.sourceCode); + assert.isUndefined(context.parserOptions); + assert.isUndefined(context.parserPath); + assert.isUndefined(context.languageOptions); + assert.isUndefined(context.settings); + }); + }); + + describe("deprecated methods", () => { + let context; + + beforeEach(() => { + context = new FileContext(defaultConfig); + }); + + it("getCwd() should return the cwd property", () => { + assert.strictEqual(context.getCwd(), context.cwd); + assert.strictEqual(context.getCwd(), defaultConfig.cwd); + }); + + it("getFilename() should return the filename property", () => { + assert.strictEqual(context.getFilename(), context.filename); + assert.strictEqual(context.getFilename(), defaultConfig.filename); + }); + + it("getPhysicalFilename() should return the physicalFilename property", () => { + assert.strictEqual( + context.getPhysicalFilename(), + context.physicalFilename, + ); + assert.strictEqual( + context.getPhysicalFilename(), + defaultConfig.physicalFilename, + ); + }); + + it("getSourceCode() should return the sourceCode property", () => { + assert.strictEqual(context.getSourceCode(), context.sourceCode); + assert.strictEqual( + context.getSourceCode(), + defaultConfig.sourceCode, + ); + }); + }); + + describe("extend()", () => { + let context; + + beforeEach(() => { + context = new FileContext(defaultConfig); + }); + + it("should create a new object with the original as prototype", () => { + const extension = { extraProperty: "extra" }; + const extended = context.extend(extension); + + // Verify new properties + assert.strictEqual(extended.extraProperty, "extra"); + + // Verify inherited properties + assert.strictEqual(extended.cwd, context.cwd); + assert.strictEqual(extended.filename, context.filename); + assert.strictEqual( + extended.physicalFilename, + context.physicalFilename, + ); + assert.strictEqual(extended.sourceCode, context.sourceCode); + assert.deepStrictEqual( + extended.parserOptions, + context.parserOptions, + ); + assert.strictEqual(extended.parserPath, context.parserPath); + assert.deepStrictEqual( + extended.languageOptions, + context.languageOptions, + ); + assert.deepStrictEqual(extended.settings, context.settings); + }); + + it("should freeze the extended object", () => { + const extension = { extraProperty: "extra" }; + const extended = context.extend(extension); + + // Verify the extended object is frozen + assert.throws(() => { + extended.cwd = "changed"; + }, TypeError); + + assert.throws(() => { + extended.extraProperty = "changed"; + }, TypeError); + }); + + it("should throw an error when attempting to override existing properties", () => { + const extension = { cwd: "newCwd" }; + + assert.throws(() => { + context.extend(extension); + }, TypeError); + }); + }); +}); From cf3635299e09570b7472286f25dacd8ab24e0517 Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Mon, 12 May 2025 18:44:47 +0200 Subject: [PATCH 21/36] chore: remove shared types (#19718) * chore: remove shared types * use `SuggestedEdit` --- lib/cli-engine/cli-engine.js | 6 +- lib/cli.js | 9 +- lib/eslint/eslint-helpers.js | 4 +- lib/eslint/eslint.js | 4 +- lib/eslint/legacy-eslint.js | 8 +- lib/linter/apply-disable-directives.js | 2 +- lib/linter/linter.js | 19 +- lib/linter/report-translator.js | 3 +- lib/rule-tester/rule-tester.js | 2 +- lib/rules/prefer-named-capture-group.js | 8 +- lib/services/processor-service.js | 2 +- lib/services/suppressions-service.js | 5 +- lib/shared/types.js | 193 ------------------ lib/types/index.d.ts | 88 ++++++++ .../fixtures/processors/pattern-processor.js | 2 + tools/check-rule-examples.js | 4 +- tools/code-sample-minimizer.js | 2 + tools/eslint-fuzzer.js | 1 + 18 files changed, 138 insertions(+), 224 deletions(-) delete mode 100644 lib/shared/types.js diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index dbaf9d3e1262..a0c3bc67bfed 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -58,15 +58,15 @@ const validFixTypes = new Set(["directive", "problem", "suggestion", "layout"]); //------------------------------------------------------------------------------ // For VSCode IntelliSense -/** @typedef {import("../shared/types").LintMessage} LintMessage */ -/** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */ -/** @typedef {import("../shared/types").RuleConf} RuleConf */ /** @typedef {import("../types").ESLint.ConfigData} ConfigData */ /** @typedef {import("../types").ESLint.DeprecatedRuleUse} DeprecatedRuleInfo */ /** @typedef {import("../types").ESLint.FormatterFunction} FormatterFunction */ +/** @typedef {import("../types").Linter.LintMessage} LintMessage */ /** @typedef {import("../types").Linter.ParserOptions} ParserOptions */ /** @typedef {import("../types").ESLint.Plugin} Plugin */ /** @typedef {import("../types").Rule.RuleModule} Rule */ +/** @typedef {import("../types").Linter.RuleEntry} RuleConf */ +/** @typedef {import("../types").Linter.SuppressedLintMessage} SuppressedLintMessage */ /** @typedef {ReturnType} ConfigArray */ /** @typedef {ReturnType} ExtractedConfig */ diff --git a/lib/cli.js b/lib/cli.js index b503587d92f9..e9133ab355ca 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -40,12 +40,13 @@ const debug = require("debug")("eslint:cli"); // Types //------------------------------------------------------------------------------ -/** @typedef {import("./eslint/eslint").ESLintOptions} ESLintOptions */ -/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */ -/** @typedef {import("./eslint/eslint").LintResult} LintResult */ +/** @import { ESLintOptions } from "./eslint/eslint.js" */ + /** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */ -/** @typedef {import("./shared/types").ResultsMeta} ResultsMeta */ +/** @typedef {import("./types").Linter.LintMessage} LintMessage */ +/** @typedef {import("./types").ESLint.LintResult} LintResult */ /** @typedef {import("./types").ESLint.Plugin} Plugin */ +/** @typedef {import("./types").ESLint.ResultsMeta} ResultsMeta */ //------------------------------------------------------------------------------ // Helpers diff --git a/lib/eslint/eslint-helpers.js b/lib/eslint/eslint-helpers.js index 80597a28b3fd..b3bfa49c0e71 100644 --- a/lib/eslint/eslint-helpers.js +++ b/lib/eslint/eslint-helpers.js @@ -30,10 +30,12 @@ const MINIMATCH_OPTIONS = { dot: true }; /** * @import { ESLintOptions } from "./eslint.js"; - * @import { LintMessage, LintResult } from "../shared/types.js"; * @import { ConfigLoader, LegacyConfigLoader } from "../config/config-loader.js"; */ +/** @typedef {import("../types").Linter.LintMessage} LintMessage */ +/** @typedef {import("../types").ESLint.LintResult} LintResult */ + /** * @typedef {Object} GlobSearch * @property {Array} patterns The normalized patterns to use for a search. diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index a9203d44a52d..66d1ed2a07de 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -57,13 +57,15 @@ const { ConfigLoader, LegacyConfigLoader } = require("../config/config-loader"); * @import { CLIEngineLintReport } from "./legacy-eslint.js"; * @import { FlatConfigArray } from "../config/flat-config-array.js"; * @import { RuleDefinition } from "@eslint/core"; - * @import { LintMessage, LintResult, ResultsMeta } from "../shared/types.js"; */ /** @typedef {ReturnType} ExtractedConfig */ /** @typedef {import("../types").Linter.Config} Config */ /** @typedef {import("../types").ESLint.DeprecatedRuleUse} DeprecatedRuleInfo */ +/** @typedef {import("../types").Linter.LintMessage} LintMessage */ +/** @typedef {import("../types").ESLint.LintResult} LintResult */ /** @typedef {import("../types").ESLint.Plugin} Plugin */ +/** @typedef {import("../types").ESLint.ResultsMeta} ResultsMeta */ /** * The options with which to configure the ESLint instance. diff --git a/lib/eslint/legacy-eslint.js b/lib/eslint/legacy-eslint.js index 0f1778ec3ec0..36d30e5a340e 100644 --- a/lib/eslint/legacy-eslint.js +++ b/lib/eslint/legacy-eslint.js @@ -30,14 +30,14 @@ const { version } = require("../../package.json"); //------------------------------------------------------------------------------ /** @typedef {import("../cli-engine/cli-engine").LintReport} CLIEngineLintReport */ -/** @typedef {import("../shared/types").LintMessage} LintMessage */ -/** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */ -/** @typedef {import("../shared/types").LintResult} LintResult */ -/** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */ /** @typedef {import("../types").ESLint.ConfigData} ConfigData */ /** @typedef {import("../types").ESLint.DeprecatedRuleUse} DeprecatedRuleInfo */ +/** @typedef {import("../types").Linter.LintMessage} LintMessage */ +/** @typedef {import("../types").ESLint.LintResult} LintResult */ /** @typedef {import("../types").ESLint.Plugin} Plugin */ +/** @typedef {import("../types").ESLint.ResultsMeta} ResultsMeta */ /** @typedef {import("../types").Rule.RuleModule} Rule */ +/** @typedef {import("../types").Linter.SuppressedLintMessage} SuppressedLintMessage */ /** * The main formatter object. diff --git a/lib/linter/apply-disable-directives.js b/lib/linter/apply-disable-directives.js index df595db57f71..95a317c3fdbc 100644 --- a/lib/linter/apply-disable-directives.js +++ b/lib/linter/apply-disable-directives.js @@ -9,7 +9,7 @@ // Typedefs //------------------------------------------------------------------------------ -/** @typedef {import("../shared/types").LintMessage} LintMessage */ +/** @typedef {import("../types").Linter.LintMessage} LintMessage */ /** @typedef {import("@eslint/core").Language} Language */ /** @typedef {import("@eslint/core").Position} Position */ /** @typedef {import("@eslint/core").RulesConfig} RulesConfig */ diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 7c04792e5ceb..7d7cada04851 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -75,18 +75,19 @@ const STEP_KIND_CALL = 2; /** @import { Language, LanguageOptions, RuleConfig, RuleDefinition, RuleSeverity } from "@eslint/core" */ -/** @typedef {import("../shared/types").Environment} Environment */ -/** @typedef {import("../shared/types").GlobalConf} GlobalConf */ -/** @typedef {import("../shared/types").LintMessage} LintMessage */ -/** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */ -/** @typedef {import("../shared/types").Processor} Processor */ -/** @typedef {import("../shared/types").Times} Times */ /** @typedef {import("../types").Linter.Config} Config */ /** @typedef {import("../types").ESLint.ConfigData} ConfigData */ +/** @typedef {import("../types").ESLint.Environment} Environment */ +/** @typedef {import("../types").Linter.GlobalConf} GlobalConf */ /** @typedef {import("../types").Linter.LanguageOptions} JSLanguageOptions */ +/** @typedef {import("../types").Linter.LintMessage} LintMessage */ +/** @typedef {import("../types").Linter.Parser} Parser */ /** @typedef {import("../types").Linter.ParserOptions} ParserOptions */ -/** @typedef {import("../types").Linter.StringSeverity} StringSeverity */ +/** @typedef {import("../types").Linter.Processor} Processor */ /** @typedef {import("../types").Rule.RuleModule} Rule */ +/** @typedef {import("../types").Linter.StringSeverity} StringSeverity */ +/** @typedef {import("../types").Linter.SuppressedLintMessage} SuppressedLintMessage */ +/** @typedef {import("../types").Linter.TimePass} TimePass */ /* eslint-disable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */ /** @@ -111,7 +112,7 @@ const STEP_KIND_CALL = 2; * @property {SourceCode|null} lastSourceCode The `SourceCode` instance that the last `verify()` call used. * @property {SuppressedLintMessage[]} lastSuppressedMessages The `SuppressedLintMessage[]` instance that the last `verify()` call produced. * @property {Map} parserMap The loaded parsers. - * @property {Times} times The times spent on applying a rule to a file (see `stats` option). + * @property {{ passes: TimePass[]; }} times The times spent on applying a rule to a file (see `stats` option). * @property {Rules} ruleMap The loaded rules. */ @@ -2618,7 +2619,7 @@ class Linter { /** * Gets the times spent on (parsing, fixing, linting) a file. - * @returns {LintTimes} The times. + * @returns {{ passes: TimePass[]; }} The times. */ getTimes() { return internalSlotsMap.get(this).times ?? { passes: [] }; diff --git a/lib/linter/report-translator.js b/lib/linter/report-translator.js index 5fb0a0d55886..f424318610de 100644 --- a/lib/linter/report-translator.js +++ b/lib/linter/report-translator.js @@ -17,7 +17,8 @@ const { interpolate } = require("./interpolate"); // Typedefs //------------------------------------------------------------------------------ -/** @typedef {import("../shared/types").LintMessage} LintMessage */ +/** @typedef {import("../types").Linter.LintMessage} LintMessage */ +/** @typedef {import("../types").Linter.LintSuggestion} SuggestionResult */ /** * An error message description diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index d2170faf72f5..5aeadb045563 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -41,7 +41,7 @@ const { SourceCode } = require("../languages/js/source-code"); /** @import { LanguageOptions, RuleDefinition } from "@eslint/core" */ -/** @typedef {import("../shared/types").Parser} Parser */ +/** @typedef {import("../types").Linter.Parser} Parser */ /** * A test case that is expected to pass lint. diff --git a/lib/rules/prefer-named-capture-group.js b/lib/rules/prefer-named-capture-group.js index e6a0a6ccb650..d5c1f46028e2 100644 --- a/lib/rules/prefer-named-capture-group.js +++ b/lib/rules/prefer-named-capture-group.js @@ -17,6 +17,12 @@ const { } = require("@eslint-community/eslint-utils"); const regexpp = require("@eslint-community/regexpp"); +//------------------------------------------------------------------------------ +// Typedefs +//------------------------------------------------------------------------------ + +/** @import { SuggestedEdit } from "@eslint/core"; */ + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ @@ -29,7 +35,7 @@ const parser = new regexpp.RegExpParser(); * @param {string} pattern The regular expression pattern to be checked. * @param {string} rawText Source text of the regexNode. * @param {ASTNode} regexNode AST node which contains the regular expression. - * @returns {Array} Fixer suggestions for the regex, if statically determinable. + * @returns {Array} Fixer suggestions for the regex, if statically determinable. */ function suggestIfPossible(groupStart, pattern, rawText, regexNode) { switch (regexNode.type) { diff --git a/lib/services/processor-service.js b/lib/services/processor-service.js index 820b944136eb..a5447a80afc7 100644 --- a/lib/services/processor-service.js +++ b/lib/services/processor-service.js @@ -17,7 +17,7 @@ const { VFile } = require("../linter/vfile.js"); // Types //----------------------------------------------------------------------------- -/** @typedef {import("../shared/types.js").LintMessage} LintMessage */ +/** @typedef {import("../types").Linter.LintMessage} LintMessage */ /** @typedef {import("../linter/vfile.js").VFile} VFile */ /** @typedef {import("@eslint/core").Language} Language */ /** @typedef {import("eslint").Linter.Processor} Processor */ diff --git a/lib/services/suppressions-service.js b/lib/services/suppressions-service.js index e72cd0da1e59..519411d4fbe4 100644 --- a/lib/services/suppressions-service.js +++ b/lib/services/suppressions-service.js @@ -19,8 +19,9 @@ const stringify = require("json-stable-stringify-without-jsonify"); //------------------------------------------------------------------------------ // For VSCode IntelliSense -/** @typedef {import("../shared/types").LintResult} LintResult */ -/** @typedef {import("../shared/types").SuppressedViolations} SuppressedViolations */ +/** @typedef {import("../types").Linter.LintMessage} LintMessage */ +/** @typedef {import("../types").ESLint.LintResult} LintResult */ +/** @typedef {Record>} SuppressedViolations */ //----------------------------------------------------------------------------- // Exports diff --git a/lib/shared/types.js b/lib/shared/types.js deleted file mode 100644 index ad253a4e1b20..000000000000 --- a/lib/shared/types.js +++ /dev/null @@ -1,193 +0,0 @@ -/** - * @fileoverview Define common types for input completion. - * @author Toru Nagashima - */ -"use strict"; - -/** @type {any} */ -module.exports = {}; - -/** @typedef {boolean | "off" | "readable" | "readonly" | "writable" | "writeable"} GlobalConf */ -/** @typedef {0 | 1 | 2 | "off" | "warn" | "error"} SeverityConf */ -/** @typedef {SeverityConf | [SeverityConf, ...any[]]} RuleConf */ - -/** - * @typedef {Object} EcmaFeatures - * @property {boolean} [globalReturn] Enabling `return` statements at the top-level. - * @property {boolean} [jsx] Enabling JSX syntax. - * @property {boolean} [impliedStrict] Enabling strict mode always. - */ - -/** - * @typedef {Object} ParserOptions - * @property {EcmaFeatures} [ecmaFeatures] The optional features. - * @property {3|5|6|7|8|9|10|11|12|13|14|15|16|2015|2016|2017|2018|2019|2020|2021|2022|2023|2024|2025} [ecmaVersion] The ECMAScript version (or revision number). - * @property {"script"|"module"} [sourceType] The source code type. - * @property {boolean} [allowReserved] Allowing the use of reserved words as identifiers in ES3. - */ - -/** - * @typedef {Object} ParseResult - * @property {Object} ast The AST. - * @property {ScopeManager} [scopeManager] The scope manager of the AST. - * @property {Record} [services] The services that the parser provides. - * @property {Record} [visitorKeys] The visitor keys of the AST. - */ - -/** - * @typedef {Object} Parser - * @property {(text:string, options:ParserOptions) => Object} parse The definition of global variables. - * @property {(text:string, options:ParserOptions) => ParseResult} [parseForESLint] The parser options that will be enabled under this environment. - */ - -/** - * @typedef {Object} Environment - * @property {Record} [globals] The definition of global variables. - * @property {ParserOptions} [parserOptions] The parser options that will be enabled under this environment. - */ - -/** - * @typedef {Object} LintMessage - * @property {number|undefined} column The 1-based column number. - * @property {number} [endColumn] The 1-based column number of the end location. - * @property {number} [endLine] The 1-based line number of the end location. - * @property {boolean} [fatal] If `true` then this is a fatal error. - * @property {{range:[number,number], text:string}} [fix] Information for autofix. - * @property {number|undefined} line The 1-based line number. - * @property {string} message The error message. - * @property {string} [messageId] The ID of the message in the rule's meta. - * @property {(string|null)} nodeType Type of node - * @property {string|null} ruleId The ID of the rule which makes this message. - * @property {0|1|2} severity The severity of this message. - * @property {Array<{desc?: string, messageId?: string, fix: {range: [number, number], text: string}}>} [suggestions] Information for suggestions. - */ - -/** - * @typedef {Object} SuppressedLintMessage - * @property {number|undefined} column The 1-based column number. - * @property {number} [endColumn] The 1-based column number of the end location. - * @property {number} [endLine] The 1-based line number of the end location. - * @property {boolean} [fatal] If `true` then this is a fatal error. - * @property {{range:[number,number], text:string}} [fix] Information for autofix. - * @property {number|undefined} line The 1-based line number. - * @property {string} message The error message. - * @property {string} [messageId] The ID of the message in the rule's meta. - * @property {(string|null)} nodeType Type of node - * @property {string|null} ruleId The ID of the rule which makes this message. - * @property {0|1|2} severity The severity of this message. - * @property {Array<{kind: string, justification: string}>} suppressions The suppression info. - * @property {Array<{desc?: string, messageId?: string, fix: {range: [number, number], text: string}}>} [suggestions] Information for suggestions. - */ - -/** - * @typedef {Record>} SuppressedViolations - */ - -/** - * @typedef {Object} SuggestionResult - * @property {string} desc A short description. - * @property {string} [messageId] Id referencing a message for the description. - * @property {{ text: string, range: number[] }} fix fix result info - */ - -/** - * @typedef {Object} Processor - * @property {(text:string, filename:string) => Array} [preprocess] The function to extract code blocks. - * @property {(messagesList:LintMessage[][], filename:string) => LintMessage[]} [postprocess] The function to merge messages. - * @property {boolean} [supportsAutofix] If `true` then it means the processor supports autofix. - */ - -/** - * @typedef {Object} RuleMetaDocs - * @property {string} description The description of the rule. - * @property {boolean} recommended If `true` then the rule is included in `eslint:recommended` preset. - * @property {string} url The URL of the rule documentation. - */ - -/** - * @typedef {Object} DeprecatedInfo - * @property {string} [message] General message presented to the user - * @property {string} [url] URL to more information about this deprecation in general - * @property {ReplacedByInfo[]} [replacedBy] Potential replacements for the rule - * @property {string} [deprecatedSince] Version since the rule is deprecated - * @property {?string} [availableUntil] Version until it is available or null if indefinite - */ - -/** - * @typedef {Object} ReplacedByInfo - * @property {string} [message] General message presented to the user - * @property {string} [url] URL to more information about this replacement in general - * @property {{ name?: string, url?: string }} [plugin] Use "eslint" for a core rule. Omit if the rule is in the same plugin. - * @property {{ name?: string, url?: string }} [rule] Name and information of the replacement rule - */ - -/** - * Information of deprecated rules. - * @typedef {Object} DeprecatedRuleInfo - * @property {string} ruleId The rule ID. - * @property {string[]} replacedBy The rule IDs that replace this deprecated rule. - * @property {DeprecatedInfo} [info] The raw deprecated info provided by rule. Unset if `deprecated` is a boolean. - */ - -/** - * A linting result. - * @typedef {Object} LintResult - * @property {string} filePath The path to the file that was linted. - * @property {LintMessage[]} messages All of the messages for the result. - * @property {SuppressedLintMessage[]} suppressedMessages All of the suppressed messages for the result. - * @property {number} errorCount Number of errors for the result. - * @property {number} fatalErrorCount Number of fatal errors for the result. - * @property {number} warningCount Number of warnings for the result. - * @property {number} fixableErrorCount Number of fixable errors for the result. - * @property {number} fixableWarningCount Number of fixable warnings for the result. - * @property {Stats} [stats] The performance statistics collected with the `stats` flag. - * @property {string} [source] The source code of the file that was linted. - * @property {string} [output] The source code of the file that was linted, with as many fixes applied as possible. - * @property {DeprecatedRuleInfo[]} usedDeprecatedRules The list of used deprecated rules. - */ - -/** - * Performance statistics - * @typedef {Object} Stats - * @property {number} fixPasses The number of times ESLint has applied at least one fix after linting. - * @property {Times} times The times spent on (parsing, fixing, linting) a file. - */ - -/** - * Performance Times for each ESLint pass - * @typedef {Object} Times - * @property {TimePass[]} passes Time passes - */ - -/** - * @typedef {Object} TimePass - * @property {ParseTime} parse The parse object containing all parse time information. - * @property {Record} [rules] The rules object containing all lint time information for each rule. - * @property {FixTime} fix The parse object containing all fix time information. - * @property {number} total The total time that is spent on (parsing, fixing, linting) a file. - */ -/** - * @typedef {Object} ParseTime - * @property {number} total The total time that is spent when parsing a file. - */ -/** - * @typedef {Object} RuleTime - * @property {number} total The total time that is spent on a rule. - */ -/** - * @typedef {Object} FixTime - * @property {number} total The total time that is spent on applying fixes to the code. - */ - -/** - * Information provided when the maximum warning threshold is exceeded. - * @typedef {Object} MaxWarningsExceeded - * @property {number} maxWarnings Number of warnings to trigger nonzero exit code. - * @property {number} foundWarnings Number of warnings found while linting. - */ - -/** - * Metadata about results for formatters. - * @typedef {Object} ResultsMeta - * @property {MaxWarningsExceeded} [maxWarningsExceeded] Present if the maxWarnings threshold was exceeded. - */ diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts index 1328b2960c87..caf9ed811451 100644 --- a/lib/types/index.d.ts +++ b/lib/types/index.d.ts @@ -1626,26 +1626,54 @@ export namespace Linter { } interface LintSuggestion { + /** A short description. */ desc: string; + + /** Fix result info. */ fix: Rule.Fix; + + /** Id referencing a message for the description. */ messageId?: string | undefined; } interface LintMessage { + /** The 1-based column number. */ column: number; + + /** The 1-based line number. */ line: number; + + /** The 1-based column number of the end location. */ endColumn?: number | undefined; + + /** The 1-based line number of the end location. */ endLine?: number | undefined; + + /** The ID of the rule which makes this message. */ ruleId: string | null; + + /** The reported message. */ message: string; + + /** The ID of the message in the rule's meta. */ messageId?: string | undefined; + /** + * Type of node. * @deprecated `nodeType` is deprecated and will be removed in the next major version. */ nodeType?: string | undefined; + + /** If `true` then this is a fatal error. */ fatal?: true | undefined; + + /** The severity of this message. */ severity: Exclude; + + /** Information for autofix. */ fix?: Rule.Fix | undefined; + + /** Information for suggestions. */ suggestions?: LintSuggestion[] | undefined; } @@ -1655,6 +1683,7 @@ export namespace Linter { } interface SuppressedLintMessage extends LintMessage { + /** The suppression info. */ suppressions: LintSuppression[]; } @@ -1716,8 +1745,13 @@ export namespace Linter { interface Processor< T extends string | ProcessorFile = string | ProcessorFile, > extends ESLint.ObjectMetaProperties { + /** If `true` then it means the processor supports autofix. */ supportsAutofix?: boolean | undefined; + + /** The function to extract code blocks. */ preprocess?(text: string, filename: string): T[]; + + /** The function to merge messages. */ postprocess?( messages: LintMessage[][], filename: string, @@ -1858,6 +1892,9 @@ export namespace Linter { reportUnusedInlineConfigs?: Severity | StringSeverity; } + /** + * Performance statistics. + */ interface Stats { /** * The number of times ESLint has applied at least one fix after linting. @@ -1871,9 +1908,24 @@ export namespace Linter { } interface TimePass { + /** + * The parse object containing all parse time information. + */ parse: { total: number }; + + /** + * The rules object containing all lint time information for each rule. + */ rules?: Record; + + /** + * The fix object containing all fix time information. + */ fix: { total: number }; + + /** + * The total time that is spent on (parsing, fixing, linting) a file. + */ total: number; } } @@ -1929,7 +1981,10 @@ export namespace ESLint { Omit, "$schema">; interface Environment { + /** The definition of global variables. */ globals?: Linter.Globals | undefined; + + /** The parser options that will be enabled under this environment. */ parserOptions?: Linter.ParserOptions | undefined; } @@ -2036,21 +2091,48 @@ export namespace ESLint { flags?: string[] | undefined; } + /** A linting result. */ interface LintResult { + /** The path to the file that was linted. */ filePath: string; + + /** All of the messages for the result. */ messages: Linter.LintMessage[]; + + /** All of the suppressed messages for the result. */ suppressedMessages: Linter.SuppressedLintMessage[]; + + /** Number of errors for the result. */ errorCount: number; + + /** Number of fatal errors for the result. */ fatalErrorCount: number; + + /** Number of warnings for the result. */ warningCount: number; + + /** Number of fixable errors for the result. */ fixableErrorCount: number; + + /** Number of fixable warnings for the result. */ fixableWarningCount: number; + + /** The source code of the file that was linted, with as many fixes applied as possible. */ output?: string | undefined; + + /** The source code of the file that was linted. */ source?: string | undefined; + + /** The performance statistics collected with the `stats` flag. */ stats?: Linter.Stats | undefined; + + /** The list of used deprecated rules. */ usedDeprecatedRules: DeprecatedRuleUse[]; } + /** + * Information provided when the maximum warning threshold is exceeded. + */ interface MaxWarningsExceeded { /** * Number of warnings to trigger nonzero exit code. @@ -2092,7 +2174,13 @@ export namespace ESLint { info?: DeprecatedInfo; } + /** + * Metadata about results for formatters. + */ interface ResultsMeta { + /** + * Present if the maxWarnings threshold was exceeded. + */ maxWarningsExceeded?: MaxWarningsExceeded | undefined; } diff --git a/tests/fixtures/processors/pattern-processor.js b/tests/fixtures/processors/pattern-processor.js index 462c591edab9..6be3624d3524 100644 --- a/tests/fixtures/processors/pattern-processor.js +++ b/tests/fixtures/processors/pattern-processor.js @@ -1,5 +1,7 @@ "use strict"; +/** @typedef {import("eslint").Linter.Processor} Processor */ + /** * Define a processor which extract code blocks `pattern` regexp matched. * The defined processor supports autofix, but doesn't have `supportsAutofix` property. diff --git a/tools/check-rule-examples.js b/tools/check-rule-examples.js index 0c345fea61e6..443692416a77 100644 --- a/tools/check-rule-examples.js +++ b/tools/check-rule-examples.js @@ -20,8 +20,8 @@ const { Linter } = require("../lib/linter"); // Typedefs //------------------------------------------------------------------------------ -/** @typedef {import("../lib/shared/types").LintMessage} LintMessage */ -/** @typedef {import("../lib/shared/types").LintResult} LintResult */ +/** @typedef {import("../lib/types").Linter.LintMessage} LintMessage */ +/** @typedef {import("../lib/types").ESLint.LintResult} LintResult */ //------------------------------------------------------------------------------ // Helpers diff --git a/tools/code-sample-minimizer.js b/tools/code-sample-minimizer.js index 1ee7b04f9ad0..7bf1c36895d8 100644 --- a/tools/code-sample-minimizer.js +++ b/tools/code-sample-minimizer.js @@ -1,5 +1,7 @@ "use strict"; +/** @typedef {import("../lib/types").Linter.Parser} Parser */ + const evk = require("eslint-visitor-keys"); const recast = require("recast"); const espree = require("espree"); diff --git a/tools/eslint-fuzzer.js b/tools/eslint-fuzzer.js index 9297a57b7491..2e8a5b6cd3da 100644 --- a/tools/eslint-fuzzer.js +++ b/tools/eslint-fuzzer.js @@ -21,6 +21,7 @@ const sampleMinimizer = require("./code-sample-minimizer"); //------------------------------------------------------------------------------ /** @typedef {import("../lib/types").ESLint.ConfigData} ConfigData */ +/** @typedef {import("../lib/types").Linter.Parser} Parser */ //------------------------------------------------------------------------------ // Helpers From 07c1a7e839ec61bd706c651428606ea5955b2bb0 Mon Sep 17 00:00:00 2001 From: sethamus <32633697+sethamus@users.noreply.github.com> Date: Tue, 13 May 2025 16:07:43 +0300 Subject: [PATCH 22/36] feat: add `allowRegexCharacters` to `no-useless-escape` (#19705) * feat: add allowedCharacters to no-useless-escape * rename option * fix examples --- docs/src/rules/no-useless-escape.md | 36 +++ lib/rules/no-useless-escape.js | 26 ++- lib/types/rules.d.ts | 8 +- tests/lib/rules/no-useless-escape.js | 318 +++++++++++++++++++++++++++ 4 files changed, 385 insertions(+), 3 deletions(-) diff --git a/docs/src/rules/no-useless-escape.md b/docs/src/rules/no-useless-escape.md index ff173c229c2f..05375e3df4ad 100644 --- a/docs/src/rules/no-useless-escape.md +++ b/docs/src/rules/no-useless-escape.md @@ -67,6 +67,42 @@ Examples of **correct** code for this rule: ::: +## Options + +This rule has an object option: + +* `allowRegexCharacters` - An array of characters that should be allowed to have unnecessary escapes in regular expressions. This is useful for characters like `-` where escaping can prevent accidental character ranges. For example, in `/[0\-]/`, the escape is technically unnecessary but helps prevent the pattern from becoming a range if another character is added later (e.g., `/[0\-9]/` vs `/[0-9]/`). + +### allowRegexCharacters + +Examples of **incorrect** code for the `{ "allowRegexCharacters": ["-"] }` option: + +::: incorrect + +```js +/*eslint no-useless-escape: ["error", { "allowRegexCharacters": ["-"] }]*/ + +/\!/; +/\@/; +/[a-z\^]/; +``` + +::: + +Examples of **correct** code for the `{ "allowRegexCharacters": ["-"] }` option: + +::: correct + +```js +/*eslint no-useless-escape: ["error", { "allowRegexCharacters": ["-"] }]*/ + +/[0\-]/; +/[\-9]/; +/a\-b/; +``` + +::: + ## When Not To Use It If you don't want to be notified about unnecessary escapes, you can safely disable this rule. diff --git a/lib/rules/no-useless-escape.js b/lib/rules/no-useless-escape.js index 4d24da4c9385..541b3a527d29 100644 --- a/lib/rules/no-useless-escape.js +++ b/lib/rules/no-useless-escape.js @@ -60,6 +60,12 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [ + { + allowRegexCharacters: [], + }, + ], + docs: { description: "Disallow unnecessary escape characters", recommended: true, @@ -78,11 +84,26 @@ module.exports = { "Replace the `\\` with `\\\\` to include the actual backslash character.", }, - schema: [], + schema: [ + { + type: "object", + properties: { + allowRegexCharacters: { + type: "array", + items: { + type: "string", + }, + uniqueItems: true, + }, + }, + additionalProperties: false, + }, + ], }, create(context) { const sourceCode = context.sourceCode; + const [{ allowRegexCharacters }] = context.options; const parser = new RegExpParser(); /** @@ -217,7 +238,8 @@ module.exports = { if ( escapedChar !== - String.fromCodePoint(characterNode.value) + String.fromCodePoint(characterNode.value) || + allowRegexCharacters.includes(escapedChar) ) { // It's a valid escape. return; diff --git a/lib/types/rules.d.ts b/lib/types/rules.d.ts index 0a5d1f0bfdb9..51844a353f73 100644 --- a/lib/types/rules.d.ts +++ b/lib/types/rules.d.ts @@ -4141,7 +4141,13 @@ export interface ESLintRules extends Linter.RulesRecord { * @since 2.5.0 * @see https://eslint.org/docs/latest/rules/no-useless-escape */ - "no-useless-escape": Linter.RuleEntry<[]>; + "no-useless-escape": Linter.RuleEntry< + [ + Partial<{ + allowRegexCharacters: string[]; + }>, + ] + >; /** * Rule to disallow renaming import, export, and destructured assignments to the same name. diff --git a/tests/lib/rules/no-useless-escape.js b/tests/lib/rules/no-useless-escape.js index 3ba64ea70342..1bdc7fbd64e8 100644 --- a/tests/lib/rules/no-useless-escape.js +++ b/tests/lib/rules/no-useless-escape.js @@ -277,6 +277,232 @@ ruleTester.run("no-useless-escape", rule, { languageOptions: { ecmaVersion: 2024 }, }, { code: String.raw`/[\^]/v`, languageOptions: { ecmaVersion: 2024 } }, + { + code: "var foo = /\\#/;", + options: [{ allowRegexCharacters: ["#"] }], + }, + { + code: "var foo = /\\;/;", + options: [{ allowRegexCharacters: [";"] }], + }, + { + code: "var foo = /\\#\\;/;", + options: [{ allowRegexCharacters: ["#", ";"] }], + }, + { + code: String.raw`var foo = /[ab\-]/`, + options: [{ allowRegexCharacters: ["-"] }], + }, + { + code: String.raw`var foo = /[\-ab]/`, + options: [{ allowRegexCharacters: ["-"] }], + }, + { + code: String.raw`var foo = /[ab\?]/`, + options: [{ allowRegexCharacters: ["?"] }], + }, + { + code: String.raw`var foo = /[ab\.]/`, + options: [{ allowRegexCharacters: ["."] }], + }, + { + code: String.raw`var foo = /[a\|b]/`, + options: [{ allowRegexCharacters: ["|"] }], + }, + { + code: String.raw`var foo = /\-/`, + options: [{ allowRegexCharacters: ["-"] }], + }, + { + code: String.raw`var foo = /[\-]/`, + options: [{ allowRegexCharacters: ["-"] }], + }, + { + code: String.raw`var foo = /[ab\$]/`, + options: [{ allowRegexCharacters: ["$"] }], + }, + { + code: String.raw`var foo = /[\(paren]/`, + options: [{ allowRegexCharacters: ["("] }], + }, + { + code: String.raw`var foo = /[\[]/`, + options: [{ allowRegexCharacters: ["["] }], + }, + { + code: String.raw`var foo = /[\/]/`, + options: [{ allowRegexCharacters: ["/"] }], + }, + { + code: String.raw`var foo = /[\B]/`, + options: [{ allowRegexCharacters: ["B"] }], + }, + { + code: String.raw`var foo = /[a][\-b]/`, + options: [{ allowRegexCharacters: ["-"] }], + }, + { + code: String.raw`var foo = /\-[]/`, + options: [{ allowRegexCharacters: ["-"] }], + }, + { + code: String.raw`var foo = /[a\^]/`, + options: [{ allowRegexCharacters: ["^"] }], + }, + { + code: String.raw`/[^\^]/`, + options: [{ allowRegexCharacters: ["^"] }], + }, + { + code: String.raw`/[^\^]/u`, + options: [{ allowRegexCharacters: ["^"] }], + languageOptions: { ecmaVersion: 2015 }, + }, + { + code: String.raw`/[\$]/v`, + options: [{ allowRegexCharacters: ["$"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\&\&]/v`, + options: [{ allowRegexCharacters: ["&"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\!!]/v`, + options: [{ allowRegexCharacters: ["!"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\##]/v`, + options: [{ allowRegexCharacters: ["#"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\%%]/v`, + options: [{ allowRegexCharacters: ["%"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\*\*]/v`, + options: [{ allowRegexCharacters: ["*"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\+\+]/v`, + options: [{ allowRegexCharacters: ["+"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\,,]/v`, + options: [{ allowRegexCharacters: [","] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\..]/v`, + options: [{ allowRegexCharacters: ["."] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\:\:]/v`, + options: [{ allowRegexCharacters: [":"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\;\;]/v`, + options: [{ allowRegexCharacters: [";"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\<\<]/v`, + options: [{ allowRegexCharacters: ["<"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\=\=]/v`, + options: [{ allowRegexCharacters: ["="] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\>\>]/v`, + options: [{ allowRegexCharacters: [">"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\?\?]/v`, + options: [{ allowRegexCharacters: ["?"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\@\@]/v`, + options: [{ allowRegexCharacters: ["@"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: "/[\\``]/v", + options: [{ allowRegexCharacters: ["`"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\~\~]/v`, + options: [{ allowRegexCharacters: ["~"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[^\^\^]/v`, + options: [{ allowRegexCharacters: ["^"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[_\^\^]/v`, + options: [{ allowRegexCharacters: ["^"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\&\&&\&]/v`, + options: [{ allowRegexCharacters: ["&"] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\p{ASCII}--\.]/v`, + options: [{ allowRegexCharacters: ["."] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\p{ASCII}&&\.]/v`, + options: [{ allowRegexCharacters: ["."] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\.--[.&]]/v`, + options: [{ allowRegexCharacters: ["."] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\.&&[.&]]/v`, + options: [{ allowRegexCharacters: ["."] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\.--\.--\.]/v`, + options: [{ allowRegexCharacters: ["."] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[\.&&\.&&\.]/v`, + options: [{ allowRegexCharacters: ["."] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[[\.&]--[\.&]]/v`, + options: [{ allowRegexCharacters: ["."] }], + languageOptions: { ecmaVersion: 2024 }, + }, + { + code: String.raw`/[[\.&]&&[\.&]]/v`, + options: [{ allowRegexCharacters: ["."] }], + languageOptions: { ecmaVersion: 2024 }, + }, ], invalid: [ @@ -2251,5 +2477,97 @@ ruleTester.run("no-useless-escape", rule, { }, ], }, + { + code: 'var foo = "\\#/";', + options: [{ allowRegexCharacters: ["#"] }], + errors: [ + { + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\#.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: 'var foo = "#/";', + }, + { + messageId: "escapeBackslash", + output: 'var foo = "\\\\#/";', + }, + ], + }, + ], + }, + { + code: "var foo = /\\#\\@/;", + options: [{ allowRegexCharacters: ["#"] }], + errors: [ + { + line: 1, + column: 14, + endColumn: 15, + message: "Unnecessary escape character: \\@.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: "var foo = /\\#@/;", + }, + { + messageId: "escapeBackslash", + output: "var foo = /\\#\\\\@/;", + }, + ], + }, + ], + }, + { + code: String.raw`var foo = /[a\@b]/`, + options: [{ allowRegexCharacters: ["#"] }], + errors: [ + { + line: 1, + column: 14, + endColumn: 15, + message: "Unnecessary escape character: \\@.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`var foo = /[a@b]/`, + }, + { + messageId: "escapeBackslash", + output: String.raw`var foo = /[a\\@b]/`, + }, + ], + }, + ], + }, + { + code: String.raw`/[\@\@]/v`, + options: [{ allowRegexCharacters: ["#"] }], + languageOptions: { ecmaVersion: 2024 }, + errors: [ + { + line: 1, + column: 3, + message: "Unnecessary escape character: \\@.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[@\@]/v`, + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\@\@]/v`, + }, + ], + }, + ], + }, ], }); From e86edee0918107e4e41e908fe59c937b83f00d4e Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 13 May 2025 11:01:13 -0400 Subject: [PATCH 23/36] refactor: Consolidate Config helpers (#19675) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Consolidate Config helpers * Update lib/config/config.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove unneeded methods * Update lib/config/config.js Co-authored-by: 루밀LuMir * Move validators cache to module level * Move getRuleNumericSeverity to static * Really move getRuleNumericSeverity to static * Remove extra files --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: 루밀LuMir --- lib/config/config.js | 333 +++++++++++- lib/config/flat-config-helpers.js | 128 ----- lib/config/rule-validator.js | 199 ------- lib/eslint/eslint.js | 7 +- lib/linter/linter.js | 21 +- lib/rule-tester/rule-tester.js | 4 +- tests/lib/config/config.js | 666 ++++++++++++++++++++++++ tests/lib/config/flat-config-helpers.js | 187 ------- 8 files changed, 1006 insertions(+), 539 deletions(-) delete mode 100644 lib/config/flat-config-helpers.js delete mode 100644 lib/config/rule-validator.js create mode 100644 tests/lib/config/config.js delete mode 100644 tests/lib/config/flat-config-helpers.js diff --git a/lib/config/config.js b/lib/config/config.js index 74ddaad3f5b5..bb638bb32ca2 100644 --- a/lib/config/config.js +++ b/lib/config/config.js @@ -10,16 +10,31 @@ //----------------------------------------------------------------------------- const { deepMergeArrays } = require("../shared/deep-merge-arrays"); -const { getRuleFromConfig } = require("./flat-config-helpers"); const { flatConfigSchema, hasMethod } = require("./flat-config-schema"); -const { RuleValidator } = require("./rule-validator"); const { ObjectSchema } = require("@eslint/config-array"); +const ajvImport = require("../shared/ajv"); +const ajv = ajvImport(); +const ruleReplacements = require("../../conf/replacements.json"); //----------------------------------------------------------------------------- -// Helpers +// Typedefs +//----------------------------------------------------------------------------- + +/** + * @import { RuleDefinition } from "@eslint/core"; + * @import { Linter } from "eslint"; + */ + //----------------------------------------------------------------------------- +// Private Members +//------------------------------------------------------------------------------ -const ruleValidator = new RuleValidator(); +// JSON schema that disallows passing any options +const noOptionsSchema = Object.freeze({ + type: "array", + minItems: 0, + maxItems: 0, +}); const severities = new Map([ [0, 0], @@ -30,6 +45,174 @@ const severities = new Map([ ["error", 2], ]); +/** + * A collection of compiled validators for rules that have already + * been validated. + * @type {WeakMap} + */ +const validators = new WeakMap(); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Throws a helpful error when a rule cannot be found. + * @param {Object} ruleId The rule identifier. + * @param {string} ruleId.pluginName The ID of the rule to find. + * @param {string} ruleId.ruleName The ID of the rule to find. + * @param {Object} config The config to search in. + * @throws {TypeError} For missing plugin or rule. + * @returns {void} + */ +function throwRuleNotFoundError({ pluginName, ruleName }, config) { + const ruleId = pluginName === "@" ? ruleName : `${pluginName}/${ruleName}`; + + const errorMessageHeader = `Key "rules": Key "${ruleId}"`; + + let errorMessage = `${errorMessageHeader}: Could not find plugin "${pluginName}" in configuration.`; + + const missingPluginErrorMessage = errorMessage; + + // if the plugin exists then we need to check if the rule exists + if (config.plugins && config.plugins[pluginName]) { + const replacementRuleName = ruleReplacements.rules[ruleName]; + + if (pluginName === "@" && replacementRuleName) { + errorMessage = `${errorMessageHeader}: Rule "${ruleName}" was removed and replaced by "${replacementRuleName}".`; + } else { + errorMessage = `${errorMessageHeader}: Could not find "${ruleName}" in plugin "${pluginName}".`; + + // otherwise, let's see if we can find the rule name elsewhere + for (const [otherPluginName, otherPlugin] of Object.entries( + config.plugins, + )) { + if (otherPlugin.rules && otherPlugin.rules[ruleName]) { + errorMessage += ` Did you mean "${otherPluginName}/${ruleName}"?`; + break; + } + } + } + + // falls through to throw error + } + + const error = new TypeError(errorMessage); + + if (errorMessage === missingPluginErrorMessage) { + error.messageTemplate = "config-plugin-missing"; + error.messageData = { pluginName, ruleId }; + } + + throw error; +} + +/** + * 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"; + } +} + +/** + * Parses a ruleId into its plugin and rule parts. + * @param {string} ruleId The rule ID to parse. + * @returns {{pluginName:string,ruleName:string}} The plugin and rule + * parts of the ruleId; + */ +function parseRuleId(ruleId) { + let pluginName, ruleName; + + // distinguish between core rules and plugin rules + if (ruleId.includes("/")) { + // mimic scoped npm packages + if (ruleId.startsWith("@")) { + pluginName = ruleId.slice(0, ruleId.lastIndexOf("/")); + } else { + pluginName = ruleId.slice(0, ruleId.indexOf("/")); + } + + ruleName = ruleId.slice(pluginName.length + 1); + } else { + pluginName = "@"; + ruleName = ruleId; + } + + return { + pluginName, + ruleName, + }; +} + +/** + * Retrieves a rule instance from a given config based on the ruleId. + * @param {string} ruleId The rule ID to look for. + * @param {Linter.Config} config The config to search. + * @returns {RuleDefinition|undefined} The rule if found + * or undefined if not. + */ +function getRuleFromConfig(ruleId, config) { + const { pluginName, ruleName } = parseRuleId(ruleId); + + return config.plugins?.[pluginName]?.rules?.[ruleName]; +} + +/** + * Gets a complete options schema for a rule. + * @param {RuleDefinition} 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.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; + } + + 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)) { + if (schema.length) { + return { + type: "array", + items: schema, + minItems: 0, + maxItems: schema.length, + }; + } + + // `schema:[]` is an explicit way to specify that the rule does not accept any options + return { ...noOptionsSchema }; + } + + // `schema:` is assumed to be a valid JSON Schema definition + return schema; +} + /** * Splits a plugin identifier in the form a/b/c into two parts: a/b and c. * @param {string} identifier The identifier to parse. @@ -124,6 +307,29 @@ function languageOptionsToJSON(languageOptions, objectKey = "languageOptions") { return result; } +/** + * Gets or creates a validator for a rule. + * @param {Object} rule The rule to get a validator for. + * @param {string} ruleId The ID of the rule (for error reporting). + * @returns {Function|null} A validation function or null if no validation is needed. + * @throws {InvalidRuleOptionsSchemaError} If a rule's `meta.schema` is invalid. + */ +function getOrCreateValidator(rule, ruleId) { + if (!validators.has(rule)) { + try { + const schema = getRuleOptionsSchema(rule); + + if (schema) { + validators.set(rule, ajv.compile(schema)); + } + } catch (err) { + throw new InvalidRuleOptionsSchemaError(ruleId, err); + } + } + + return validators.get(rule); +} + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- @@ -252,7 +458,7 @@ class Config { // Process the rules if (this.rules) { this.#normalizeRulesConfig(); - ruleValidator.validate(this); + this.validateRulesConfig(this.rules); } } @@ -291,6 +497,15 @@ class Config { }; } + /** + * Gets a rule configuration by its ID. + * @param {string} ruleId The ID of the rule to get. + * @returns {RuleDefinition|undefined} The rule definition from the plugin, or `undefined` if the rule is not found. + */ + getRuleDefinition(ruleId) { + return getRuleFromConfig(ruleId, this); + } + /** * Normalizes the rules configuration. Ensures that each rule config is * an array and that the severity is a number. Applies meta.defaultOptions. @@ -323,6 +538,114 @@ class Config { this.rules[ruleId] = ruleConfig; } } + + /** + * Validates all of the rule configurations in the given rules config + * against the plugins in this instance. This is used primarily to + * validate inline configuration rules while inting. + * @param {Object} rulesConfig The rules config to validate. + * @returns {void} + * @throws {Error} If a rule's configuration does not match its schema. + * @throws {TypeError} If the rulesConfig is not provided or is invalid. + * @throws {InvalidRuleOptionsSchemaError} If a rule's `meta.schema` is invalid. + * @throws {TypeError} If a rule is not found in the plugins. + */ + validateRulesConfig(rulesConfig) { + if (!rulesConfig) { + throw new TypeError("Config is required for validation."); + } + + for (const [ruleId, ruleOptions] of Object.entries(rulesConfig)) { + // check for edge case + if (ruleId === "__proto__") { + continue; + } + + /* + * If a rule is disabled, we don't do any validation. This allows + * users to safely set any value to 0 or "off" without worrying + * that it will cause a validation error. + * + * Note: ruleOptions is always an array at this point because + * this validation occurs after FlatConfigArray has merged and + * normalized values. + */ + if (ruleOptions[0] === 0) { + continue; + } + + const rule = getRuleFromConfig(ruleId, this); + + if (!rule) { + throwRuleNotFoundError(parseRuleId(ruleId), this); + } + + const validateRule = getOrCreateValidator(rule, ruleId); + + if (validateRule) { + validateRule(ruleOptions.slice(1)); + + if (validateRule.errors) { + throw new Error( + `Key "rules": Key "${ruleId}":\n${validateRule.errors + .map(error => { + if ( + error.keyword === "additionalProperties" && + error.schema === false && + typeof error.parentSchema?.properties === + "object" && + typeof error.params?.additionalProperty === + "string" + ) { + const expectedProperties = Object.keys( + error.parentSchema.properties, + ).map(property => `"${property}"`); + + return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n\t\tUnexpected property "${error.params.additionalProperty}". Expected properties: ${expectedProperties.join(", ")}.\n`; + } + + return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`; + }) + .join("")}`, + ); + } + } + } + } + + /** + * Gets a complete options schema for a rule. + * @param {RuleDefinition} ruleDefinition A rule definition 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`. + */ + static getRuleOptionsSchema(ruleDefinition) { + return getRuleOptionsSchema(ruleDefinition); + } + + /** + * Normalizes the severity value of a rule's configuration to a number + * @param {(number|string|[number, ...*]|[string, ...*])} ruleConfig A rule's configuration value, generally + * received from the user. A valid config value is either 0, 1, 2, the string "off" (treated the same as 0), + * the string "warn" (treated the same as 1), the string "error" (treated the same as 2), or an array + * whose first element is one of the above values. Strings are matched case-insensitively. + * @returns {(0|1|2)} The numeric severity value if the config value was valid, otherwise 0. + */ + static getRuleNumericSeverity(ruleConfig) { + const severityValue = Array.isArray(ruleConfig) + ? ruleConfig[0] + : ruleConfig; + + if (severities.has(severityValue)) { + return severities.get(severityValue); + } + + if (typeof severityValue === "string") { + return severities.get(severityValue.toLowerCase()) ?? 0; + } + + return 0; + } } module.exports = { Config }; diff --git a/lib/config/flat-config-helpers.js b/lib/config/flat-config-helpers.js deleted file mode 100644 index 430a3587fb0c..000000000000 --- a/lib/config/flat-config-helpers.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @fileoverview Shared functions to work with configs. - * @author Nicholas C. Zakas - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Typedefs -//------------------------------------------------------------------------------ - -/** - * @import { RuleDefinition } from "@eslint/core"; - * @import { Linter } from "eslint"; - */ - -//------------------------------------------------------------------------------ -// Private Members -//------------------------------------------------------------------------------ - -// JSON schema that disallows passing any options -const noOptionsSchema = Object.freeze({ - type: "array", - minItems: 0, - maxItems: 0, -}); - -//----------------------------------------------------------------------------- -// Functions -//----------------------------------------------------------------------------- - -/** - * Parses a ruleId into its plugin and rule parts. - * @param {string} ruleId The rule ID to parse. - * @returns {{pluginName:string,ruleName:string}} The plugin and rule - * parts of the ruleId; - */ -function parseRuleId(ruleId) { - let pluginName, ruleName; - - // distinguish between core rules and plugin rules - if (ruleId.includes("/")) { - // mimic scoped npm packages - if (ruleId.startsWith("@")) { - pluginName = ruleId.slice(0, ruleId.lastIndexOf("/")); - } else { - pluginName = ruleId.slice(0, ruleId.indexOf("/")); - } - - ruleName = ruleId.slice(pluginName.length + 1); - } else { - pluginName = "@"; - ruleName = ruleId; - } - - return { - pluginName, - ruleName, - }; -} - -/** - * Retrieves a rule instance from a given config based on the ruleId. - * @param {string} ruleId The rule ID to look for. - * @param {Linter.Config} config The config to search. - * @returns {RuleDefinition|undefined} The rule if found - * or undefined if not. - */ -function getRuleFromConfig(ruleId, config) { - const { pluginName, ruleName } = parseRuleId(ruleId); - - return config.plugins?.[pluginName]?.rules?.[ruleName]; -} - -/** - * Gets a complete options schema for a rule. - * @param {RuleDefinition} 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.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; - } - - 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)) { - if (schema.length) { - return { - type: "array", - items: schema, - minItems: 0, - maxItems: schema.length, - }; - } - - // `schema:[]` is an explicit way to specify that the rule does not accept any options - return { ...noOptionsSchema }; - } - - // `schema:` is assumed to be a valid JSON Schema definition - return schema; -} - -//----------------------------------------------------------------------------- -// Exports -//----------------------------------------------------------------------------- - -module.exports = { - parseRuleId, - getRuleFromConfig, - getRuleOptionsSchema, -}; diff --git a/lib/config/rule-validator.js b/lib/config/rule-validator.js deleted file mode 100644 index c4910a16572d..000000000000 --- a/lib/config/rule-validator.js +++ /dev/null @@ -1,199 +0,0 @@ -/** - * @fileoverview Rule Validator - * @author Nicholas C. Zakas - */ - -"use strict"; - -//----------------------------------------------------------------------------- -// Requirements -//----------------------------------------------------------------------------- - -const ajvImport = require("../shared/ajv"); -const ajv = ajvImport(); -const { - parseRuleId, - getRuleFromConfig, - getRuleOptionsSchema, -} = require("./flat-config-helpers"); -const ruleReplacements = require("../../conf/replacements.json"); - -//----------------------------------------------------------------------------- -// Helpers -//----------------------------------------------------------------------------- - -/** - * Throws a helpful error when a rule cannot be found. - * @param {Object} ruleId The rule identifier. - * @param {string} ruleId.pluginName The ID of the rule to find. - * @param {string} ruleId.ruleName The ID of the rule to find. - * @param {Object} config The config to search in. - * @throws {TypeError} For missing plugin or rule. - * @returns {void} - */ -function throwRuleNotFoundError({ pluginName, ruleName }, config) { - const ruleId = pluginName === "@" ? ruleName : `${pluginName}/${ruleName}`; - - const errorMessageHeader = `Key "rules": Key "${ruleId}"`; - - let errorMessage = `${errorMessageHeader}: Could not find plugin "${pluginName}" in configuration.`; - - const missingPluginErrorMessage = errorMessage; - - // if the plugin exists then we need to check if the rule exists - if (config.plugins && config.plugins[pluginName]) { - const replacementRuleName = ruleReplacements.rules[ruleName]; - - if (pluginName === "@" && replacementRuleName) { - errorMessage = `${errorMessageHeader}: Rule "${ruleName}" was removed and replaced by "${replacementRuleName}".`; - } else { - errorMessage = `${errorMessageHeader}: Could not find "${ruleName}" in plugin "${pluginName}".`; - - // otherwise, let's see if we can find the rule name elsewhere - for (const [otherPluginName, otherPlugin] of Object.entries( - config.plugins, - )) { - if (otherPlugin.rules && otherPlugin.rules[ruleName]) { - errorMessage += ` Did you mean "${otherPluginName}/${ruleName}"?`; - break; - } - } - } - - // falls through to throw error - } - - const error = new TypeError(errorMessage); - - if (errorMessage === missingPluginErrorMessage) { - error.messageTemplate = "config-plugin-missing"; - error.messageData = { pluginName, ruleId }; - } - - throw error; -} - -/** - * 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"; - } -} - -//----------------------------------------------------------------------------- -// Exports -//----------------------------------------------------------------------------- - -/** - * Implements validation functionality for the rules portion of a config. - */ -class RuleValidator { - /** - * Creates a new instance. - */ - constructor() { - /** - * A collection of compiled validators for rules that have already - * been validated. - * @type {WeakMap} - */ - this.validators = new WeakMap(); - } - - /** - * Validates all of the rule configurations in a config against each - * rule's schema. - * @param {Object} config The full config to validate. This object must - * contain both the rules section and the plugins section. - * @returns {void} - * @throws {Error} If a rule's configuration does not match its schema. - */ - validate(config) { - if (!config.rules) { - return; - } - - for (const [ruleId, ruleOptions] of Object.entries(config.rules)) { - // check for edge case - if (ruleId === "__proto__") { - continue; - } - - /* - * If a rule is disabled, we don't do any validation. This allows - * users to safely set any value to 0 or "off" without worrying - * that it will cause a validation error. - * - * Note: ruleOptions is always an array at this point because - * this validation occurs after FlatConfigArray has merged and - * normalized values. - */ - if (ruleOptions[0] === 0) { - continue; - } - - const rule = getRuleFromConfig(ruleId, config); - - if (!rule) { - throwRuleNotFoundError(parseRuleId(ruleId), config); - } - - // Precompile and cache validator the first time - if (!this.validators.has(rule)) { - try { - const schema = getRuleOptionsSchema(rule); - - if (schema) { - this.validators.set(rule, ajv.compile(schema)); - } - } catch (err) { - throw new InvalidRuleOptionsSchemaError(ruleId, err); - } - } - - const validateRule = this.validators.get(rule); - - if (validateRule) { - validateRule(ruleOptions.slice(1)); - - if (validateRule.errors) { - throw new Error( - `Key "rules": Key "${ruleId}":\n${validateRule.errors - .map(error => { - if ( - error.keyword === "additionalProperties" && - error.schema === false && - typeof error.parentSchema?.properties === - "object" && - typeof error.params?.additionalProperty === - "string" - ) { - const expectedProperties = Object.keys( - error.parentSchema.properties, - ).map(property => `"${property}"`); - - return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n\t\tUnexpected property "${error.params.additionalProperty}". Expected properties: ${expectedProperties.join(", ")}.\n`; - } - - return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`; - }) - .join("")}`, - ); - } - } - } - } -} - -exports.RuleValidator = RuleValidator; diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 66d1ed2a07de..d7a94773869a 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -14,7 +14,6 @@ const { existsSync } = require("node:fs"); const path = require("node:path"); const { version } = require("../../package.json"); const { Linter } = require("../linter"); -const { getRuleFromConfig } = require("../config/flat-config-helpers"); const { defaultConfig } = require("../config/default-config"); const { Legacy: { @@ -163,7 +162,7 @@ function getOrFindUsedDeprecatedRules(eslint, maybeFilePath) { if (getRuleSeverity(ruleConf) === 0) { continue; } - const rule = getRuleFromConfig(ruleId, config); + const rule = config.getRuleDefinition(ruleId); const meta = rule && rule.meta; if (meta && meta.deprecated) { @@ -357,7 +356,7 @@ function shouldMessageBeFixed(message, config, fixTypes) { return fixTypes.has("directive"); } - const rule = message.ruleId && getRuleFromConfig(message.ruleId, config); + const rule = message.ruleId && config.getRuleDefinition(message.ruleId); return Boolean(rule && rule.meta && fixTypes.has(rule.meta.type)); } @@ -615,7 +614,7 @@ class ESLint { if (!config) { throw createExtraneousResultsError(); } - const rule = getRuleFromConfig(ruleId, config); + const rule = config.getRuleDefinition(ruleId); // ignore unknown rules if (rule) { diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 7d7cada04851..466ddff56a67 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -34,10 +34,8 @@ const path = require("node:path"), SourceCodeFixer = require("./source-code-fixer"), timing = require("./timing"), ruleReplacements = require("../../conf/replacements.json"); -const { getRuleFromConfig } = require("../config/flat-config-helpers"); const { FlatConfigArray } = require("../config/flat-config-array"); const { startTime, endTime } = require("../shared/stats"); -const { RuleValidator } = require("../config/rule-validator"); const { assertIsRuleSeverity } = require("../config/flat-config-schema"); const { normalizeSeverityToString, @@ -66,6 +64,7 @@ const { ParserService } = require("../services/parser-service"); const { FileContext } = require("./file-context"); const { ProcessorService } = require("../services/processor-service"); const { containsDifferentProperty } = require("../shared/option-utils"); +const { Config } = require("../config/config"); const STEP_KIND_VISIT = 1; const STEP_KIND_CALL = 2; @@ -1202,7 +1201,7 @@ function runRules( const lintingProblems = []; Object.keys(configuredRules).forEach(ruleId => { - const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]); + const severity = Config.getRuleNumericSeverity(configuredRules[ruleId]); // not load disabled rules if (severity === 0) { @@ -2100,15 +2099,12 @@ class Linter { }), ); - // next we need to verify information about the specified rules - const ruleValidator = new RuleValidator(); - for (const { config: inlineConfig, loc, } of inlineConfigResult.configs) { Object.keys(inlineConfig.rules).forEach(ruleId => { - const rule = getRuleFromConfig(ruleId, config); + const rule = config.getRuleDefinition(ruleId); const ruleValue = inlineConfig.rules[ruleId]; if (!rule) { @@ -2218,11 +2214,8 @@ class Linter { } if (shouldValidateOptions) { - ruleValidator.validate({ - plugins: config.plugins, - rules: { - [ruleId]: ruleOptions, - }, + config.validateRulesConfig({ + [ruleId]: ruleOptions, }); } @@ -2270,7 +2263,7 @@ class Linter { options.allowInlineConfig && !options.warnInlineConfig ? getDirectiveCommentsForFlatConfig( sourceCode, - ruleId => getRuleFromConfig(ruleId, config), + ruleId => config.getRuleDefinition(ruleId), config.language, ) : { problems: [], disableDirectives: [] }; @@ -2289,7 +2282,7 @@ class Linter { lintingProblems = runRules( sourceCode, configuredRules, - ruleId => getRuleFromConfig(ruleId, config), + ruleId => config.getRuleDefinition(ruleId), void 0, config.language, languageOptions, diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 5aeadb045563..382a3d19e8e0 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -15,7 +15,7 @@ const assert = require("node:assert"), path = require("node:path"), equal = require("fast-deep-equal"), Traverser = require("../shared/traverser"), - { getRuleOptionsSchema } = require("../config/flat-config-helpers"), + { Config } = require("../config/config"), { Linter, SourceCodeFixer } = require("../linter"), { interpolate, getPlaceholderMatcher } = require("../linter/interpolate"), stringify = require("json-stable-stringify-without-jsonify"); @@ -767,7 +767,7 @@ class RuleTester { let schema; try { - schema = getRuleOptionsSchema(rule); + schema = Config.getRuleOptionsSchema(rule); } catch (err) { err.message += metaSchemaDescription; throw err; diff --git a/tests/lib/config/config.js b/tests/lib/config/config.js new file mode 100644 index 000000000000..c7ad77e852ed --- /dev/null +++ b/tests/lib/config/config.js @@ -0,0 +1,666 @@ +/** + * @fileoverview Tests for Config + * @author Nicholas C. Zakas + */ + +/* eslint no-new: "off" -- new is needed to test constructor */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const { Config } = require("../../../lib/config/config"); +const assert = require("chai").assert; +const sinon = require("sinon"); + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("Config", () => { + describe("static getRuleOptionsSchema", () => { + const noOptionsSchema = { + type: "array", + minItems: 0, + maxItems: 0, + }; + + it("should return schema that doesn't accept options if rule doesn't have `meta`", () => { + const rule = {}; + const result = Config.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return schema that doesn't accept options if rule doesn't have `meta.schema`", () => { + const rule = { meta: {} }; + const result = Config.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return schema that doesn't accept options if `meta.schema` is `undefined`", () => { + const rule = { meta: { schema: void 0 } }; + const result = Config.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return schema that doesn't accept options if `meta.schema` is `[]`", () => { + const rule = { meta: { schema: [] } }; + const result = Config.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return JSON Schema definition object if `meta.schema` is in the array form", () => { + const firstOption = { enum: ["always", "never"] }; + const rule = { meta: { schema: [firstOption] } }; + const result = Config.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, { + type: "array", + items: [firstOption], + minItems: 0, + maxItems: 1, + }); + }); + + it("should return `meta.schema` as is if `meta.schema` is an object", () => { + const schema = { + type: "array", + items: [ + { + enum: ["always", "never"], + }, + ], + }; + const rule = { meta: { schema } }; + const result = Config.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, schema); + }); + + it("should return `null` if `meta.schema` is `false`", () => { + const rule = { meta: { schema: false } }; + const result = Config.getRuleOptionsSchema(rule); + + assert.strictEqual(result, null); + }); + + [null, true, 0, 1, "", "always", () => {}].forEach(schema => { + it(`should throw an error if \`meta.schema\` is ${typeof schema} ${schema}`, () => { + const rule = { meta: { schema } }; + + assert.throws(() => { + Config.getRuleOptionsSchema(rule); + }, "Rule's `meta.schema` must be an array or object"); + }); + }); + + it("should ignore top-level `schema` property", () => { + const rule = { schema: { enum: ["always", "never"] } }; + const result = Config.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + }); + + describe("static getRuleNumericSeverity", () => { + it("should return 0 for 'off'", () => { + const result = Config.getRuleNumericSeverity("off"); + assert.strictEqual(result, 0); + }); + + it("should return 1 for 'warn'", () => { + const result = Config.getRuleNumericSeverity("warn"); + assert.strictEqual(result, 1); + }); + + it("should return 2 for 'error'", () => { + const result = Config.getRuleNumericSeverity("error"); + assert.strictEqual(result, 2); + }); + + it("should return 0 for 0", () => { + const result = Config.getRuleNumericSeverity(0); + assert.strictEqual(result, 0); + }); + + it("should return 1 for 1", () => { + const result = Config.getRuleNumericSeverity(1); + assert.strictEqual(result, 1); + }); + + it("should return 2 for 2", () => { + const result = Config.getRuleNumericSeverity(2); + assert.strictEqual(result, 2); + }); + + it("should handle rule config arrays", () => { + const result = Config.getRuleNumericSeverity([ + "error", + { option: true }, + ]); + assert.strictEqual(result, 2); + }); + + it("should be case-insensitive for string values", () => { + const result = Config.getRuleNumericSeverity("ERROR"); + assert.strictEqual(result, 2); + }); + + it("should return 0 for invalid severity strings", () => { + const result = Config.getRuleNumericSeverity("invalid"); + assert.strictEqual(result, 0); + }); + + it("should return 0 for non-severity values", () => { + const result = Config.getRuleNumericSeverity(null); + assert.strictEqual(result, 0); + }); + }); + + describe("constructor", () => { + let mockLanguage; + + beforeEach(() => { + mockLanguage = { + validateLanguageOptions: sinon.stub(), + normalizeLanguageOptions: sinon.spy(options => options), + }; + }); + + it("should throw error when language is not provided", () => { + assert.throws(() => { + new Config({}); + }, "Key 'language' is required."); + }); + + it("should throw error when language is not found in plugins", () => { + assert.throws(() => { + new Config({ + language: "test/lang", + plugins: { + test: { + // No languages + }, + }, + }); + }, /Could not find "lang" in plugin "test"/u); + }); + + it("should correctly set up language from plugins", () => { + const config = new Config({ + language: "test/lang", + plugins: { + test: { + languages: { + lang: mockLanguage, + }, + }, + }, + }); + + assert.strictEqual(config.language, mockLanguage); + assert.isTrue(mockLanguage.validateLanguageOptions.called); + }); + + it("should correctly merge language options with default language options", () => { + mockLanguage.defaultLanguageOptions = { parser: "default" }; + + const config = new Config({ + language: "test/lang", + languageOptions: { ecmaVersion: 2022 }, + plugins: { + test: { + languages: { + lang: mockLanguage, + }, + }, + }, + }); + + assert.deepStrictEqual(config.languageOptions, { + parser: "default", + ecmaVersion: 2022, + }); + }); + + it("should throw error when processor is not found in plugins", () => { + assert.throws(() => { + new Config({ + language: "test/lang", + plugins: { + test: { + languages: { + lang: mockLanguage, + }, + }, + }, + processor: "test/proc", + }); + }, /Could not find "proc" in plugin "test"/u); + }); + + it("should correctly set up processor from plugins", () => { + const mockProcessor = {}; + const config = new Config({ + language: "test/lang", + plugins: { + test: { + languages: { + lang: mockLanguage, + }, + processors: { + proc: mockProcessor, + }, + }, + }, + processor: "test/proc", + }); + + assert.strictEqual(config.processor, mockProcessor); + }); + + it("should accept processor object directly", () => { + const mockProcessor = { + meta: { name: "test-processor" }, + preprocess() {}, + postprocess() {}, + }; + + const config = new Config({ + language: "test/lang", + plugins: { + test: { + languages: { + lang: mockLanguage, + }, + }, + }, + processor: mockProcessor, + }); + + assert.strictEqual(config.processor, mockProcessor); + }); + + it("should throw error when processor is not string or object", () => { + assert.throws(() => { + new Config({ + language: "test/lang", + plugins: { + test: { + languages: { + lang: mockLanguage, + }, + }, + }, + processor: 123, + }); + }, "Expected an object or a string"); + }); + + it("should normalize rules configuration", () => { + const mockRule = { meta: {} }; + const config = new Config({ + language: "test/lang", + plugins: { + test: { + languages: { + lang: mockLanguage, + }, + rules: {}, + }, + "@": { + rules: { + "test-rule": mockRule, + }, + }, + }, + rules: { + "test-rule": "error", + }, + }); + + assert.deepStrictEqual(config.rules["test-rule"], [2]); + }); + + it("should normalize rules with options", () => { + const mockRule = { + meta: { + schema: [ + { + type: "object", + properties: { + option1: { type: "boolean" }, + }, + }, + ], + }, + }; + const config = new Config({ + language: "test/lang", + plugins: { + test: { + languages: { + lang: mockLanguage, + }, + rules: {}, + }, + "@": { + rules: { + "test-rule": mockRule, + }, + }, + }, + rules: { + "test-rule": ["warn", { option1: true }], + }, + }); + + assert.deepStrictEqual(config.rules["test-rule"], [ + 1, + { option1: true }, + ]); + }); + + it("should apply rule's defaultOptions when present", () => { + const mockRule = { + meta: { + schema: [ + { + type: "object", + properties: { + option1: { type: "boolean" }, + defaultOption: { type: "boolean" }, + }, + }, + ], + defaultOptions: [{ defaultOption: true }], + }, + }; + + const config = new Config({ + language: "test/lang", + plugins: { + test: { + languages: { + lang: mockLanguage, + }, + rules: {}, + }, + "@": { + rules: { + "test-rule": mockRule, + }, + }, + }, + rules: { + "test-rule": ["error", { option1: true }], + }, + }); + + assert.deepStrictEqual(config.rules["test-rule"], [ + 2, + { defaultOption: true, option1: true }, + ]); + }); + }); + + describe("getRuleDefinition", () => { + it("should retrieve rule definition from plugins", () => { + const mockRule = { meta: {} }; + const config = new Config({ + language: "test/lang", + plugins: { + test: { + languages: { + lang: { validateLanguageOptions() {} }, + }, + rules: { + "test-rule": mockRule, + }, + }, + }, + }); + + const rule = config.getRuleDefinition("test/test-rule"); + assert.strictEqual(rule, mockRule); + }); + + it("should retrieve core rule definition", () => { + const mockRule = { meta: {} }; + const config = new Config({ + language: "test/lang", + plugins: { + test: { + languages: { + lang: { validateLanguageOptions() {} }, + }, + }, + "@": { + rules: { + "core-rule": mockRule, + }, + }, + }, + }); + + const rule = config.getRuleDefinition("core-rule"); + assert.strictEqual(rule, mockRule); + }); + }); + + describe("toJSON", () => { + it("should convert config to JSON representation", () => { + const mockLanguage = { + validateLanguageOptions() {}, + meta: { + name: "testLang", + version: "1.0.0", + }, + }; + + const mockProcessor = { + meta: { + name: "testProcessor", + version: "1.0.0", + }, + preprocess() {}, + postprocess() {}, + }; + + const mockPlugin = { + meta: { + name: "testPlugin", + version: "1.0.0", + }, + }; + + const config = new Config({ + language: "test/lang", + plugins: { + test: { + ...mockPlugin, + languages: { + lang: mockLanguage, + }, + }, + }, + processor: mockProcessor, + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + parser: { + meta: { + name: "testParser", + }, + parse() {}, + }, + }, + }); + + const json = config.toJSON(); + + assert.deepStrictEqual(json.plugins, ["test:testPlugin@1.0.0"]); + assert.strictEqual(json.language, "test/lang"); + assert.strictEqual(json.processor, "testProcessor@1.0.0"); + assert.deepStrictEqual(json.languageOptions, { + ecmaVersion: 2022, + sourceType: "module", + parser: "testParser", + }); + }); + + it("should throw when processor doesn't have meta information", () => { + const mockLanguage = { + validateLanguageOptions() {}, + meta: { + name: "testLang", + }, + }; + + const mockProcessor = { + preprocess() {}, + postprocess() {}, + }; // Missing meta property + + const config = new Config({ + language: "test/lang", + plugins: { + test: { + languages: { + lang: mockLanguage, + }, + }, + }, + processor: mockProcessor, + }); + + assert.throws(() => { + config.toJSON(); + }, "Could not serialize processor object (missing 'meta' object)."); + }); + }); + + describe("validateRulesConfig", () => { + let config; + + const mockRule = { + meta: { + schema: { + type: "array", + items: [ + { + type: "object", + properties: { + valid: { type: "boolean" }, + }, + additionalProperties: false, + }, + ], + }, + }, + }; + + beforeEach(() => { + config = new Config({ + language: "test/lang", + plugins: { + test: { + languages: { + lang: { validateLanguageOptions() {} }, + }, + }, + "@": { + rules: { + "error-rule": {}, + "warn-rule": {}, + "off-rule": {}, + "test-rule": mockRule, + "test-broken-rule": { + meta: { schema: 123 }, // Invalid schema + }, + "test-no-schema": { + meta: { schema: false }, // No schema + }, + }, + }, + }, + rules: { + "error-rule": "error", + "warn-rule": "warn", + "off-rule": "off", + }, + }); + }); + + it("should throw when config is not provided", () => { + assert.throws(() => { + config.validateRulesConfig(); + }, "Config is required for validation."); + }); + + it("should not validate disabled rules", () => { + // This should not throw + config.validateRulesConfig({ + "error-rule": ["off"], + }); + }); + + it("should throw when rule is not found", () => { + assert.throws(() => { + config.validateRulesConfig({ + "test/missing-rule": ["error"], + }); + }, /Could not find "missing-rule" in plugin "test"/u); + }); + + it("should throw when rule options don't match schema", () => { + assert.throws(() => { + config.validateRulesConfig({ + "test-rule": ["error", { invalid: true }], + }); + }, /Unexpected property "invalid"/u); + }); + + it("should throw when rule schema is invalid", () => { + assert.throws(() => { + config.validateRulesConfig({ + "test-broken-rule": ["error"], + }); + }, /Rule's `meta.schema` must be an array or object/u); + }); + + it("should validate rule options successfully", () => { + config.validateRulesConfig({ + "test-rule": ["error", { valid: true }], + }); + }); + + it("should skip validation when `meta.schema` is false", () => { + // This should not throw, even with invalid options + config.validateRulesConfig({ + "test-no-schema": [ + "error", + "this", + "would", + "normally", + "fail", + ], + }); + }); + + it("should skip __proto__ in rules", () => { + const rules = { "test-rule": ["error"] }; + + /* eslint-disable-next-line no-proto -- Testing __proto__ behavior */ + rules.__proto__ = ["error"]; + + config.validateRulesConfig(rules); + }); + }); +}); diff --git a/tests/lib/config/flat-config-helpers.js b/tests/lib/config/flat-config-helpers.js deleted file mode 100644 index e0e9aa89862c..000000000000 --- a/tests/lib/config/flat-config-helpers.js +++ /dev/null @@ -1,187 +0,0 @@ -/** - * @fileoverview Tests for FlatConfigArray - * @author Nicholas C. Zakas - */ - -"use strict"; - -//----------------------------------------------------------------------------- -// Requirements -//----------------------------------------------------------------------------- - -const { - parseRuleId, - getRuleFromConfig, - getRuleOptionsSchema, -} = require("../../../lib/config/flat-config-helpers"); -const assert = require("chai").assert; - -//----------------------------------------------------------------------------- -// Tests -//----------------------------------------------------------------------------- - -describe("Config Helpers", () => { - describe("parseRuleId()", () => { - it("should return plugin name and rule name for core rule", () => { - const result = parseRuleId("foo"); - - assert.deepStrictEqual(result, { - pluginName: "@", - ruleName: "foo", - }); - }); - - it("should return plugin name and rule name with a/b format", () => { - const result = parseRuleId("test/foo"); - - assert.deepStrictEqual(result, { - pluginName: "test", - ruleName: "foo", - }); - }); - - it("should return plugin name and rule name with a/b/c format", () => { - const result = parseRuleId( - "node/no-unsupported-features/es-builtins", - ); - - assert.deepStrictEqual(result, { - pluginName: "node", - ruleName: "no-unsupported-features/es-builtins", - }); - }); - - it("should return plugin name and rule name with @a/b/c format", () => { - const result = parseRuleId("@test/foo/bar"); - - assert.deepStrictEqual(result, { - pluginName: "@test/foo", - ruleName: "bar", - }); - }); - }); - - describe("getRuleFromConfig", () => { - it("should retrieve rule from plugin in config", () => { - const rule = {}; - const config = { - plugins: { - test: { - rules: { - one: rule, - }, - }, - }, - }; - - const result = getRuleFromConfig("test/one", config); - - assert.strictEqual(result, rule); - }); - - it("should retrieve rule from core in config", () => { - const rule = {}; - const config = { - plugins: { - "@": { - rules: { - semi: rule, - }, - }, - }, - }; - - const result = getRuleFromConfig("semi", config); - - assert.strictEqual(result, rule); - }); - }); - - describe("getRuleOptionsSchema", () => { - const noOptionsSchema = { - type: "array", - minItems: 0, - maxItems: 0, - }; - - it("should return schema that doesn't accept options if rule doesn't have `meta`", () => { - const rule = {}; - const result = getRuleOptionsSchema(rule); - - assert.deepStrictEqual(result, noOptionsSchema); - }); - - it("should return schema that doesn't accept options if rule doesn't have `meta.schema`", () => { - const rule = { meta: {} }; - const result = getRuleOptionsSchema(rule); - - assert.deepStrictEqual(result, noOptionsSchema); - }); - - it("should return schema that doesn't accept options if `meta.schema` is `undefined`", () => { - const rule = { meta: { schema: void 0 } }; - const result = getRuleOptionsSchema(rule); - - assert.deepStrictEqual(result, noOptionsSchema); - }); - - it("should return schema that doesn't accept options if `meta.schema` is `[]`", () => { - const rule = { meta: { schema: [] } }; - const result = getRuleOptionsSchema(rule); - - assert.deepStrictEqual(result, noOptionsSchema); - }); - - it("should return JSON Schema definition object if `meta.schema` is in the array form", () => { - const firstOption = { enum: ["always", "never"] }; - const rule = { meta: { schema: [firstOption] } }; - const result = getRuleOptionsSchema(rule); - - assert.deepStrictEqual(result, { - type: "array", - items: [firstOption], - minItems: 0, - maxItems: 1, - }); - }); - - it("should return `meta.schema` as is if `meta.schema` is an object", () => { - const schema = { - type: "array", - items: [ - { - enum: ["always", "never"], - }, - ], - }; - const rule = { meta: { schema } }; - const result = getRuleOptionsSchema(rule); - - assert.deepStrictEqual(result, schema); - }); - - it("should return `null` if `meta.schema` is `false`", () => { - const rule = { meta: { schema: false } }; - const result = getRuleOptionsSchema(rule); - - assert.strictEqual(result, null); - }); - - [null, true, 0, 1, "", "always", () => {}].forEach(schema => { - it(`should throw an error if \`meta.schema\` is ${typeof schema} ${schema}`, () => { - const rule = { meta: { schema } }; - - assert.throws(() => { - getRuleOptionsSchema(rule); - }, "Rule's `meta.schema` must be an array or object"); - }); - }); - - it("should ignore top-level `schema` property", () => { - const rule = { schema: { enum: ["always", "never"] } }; - const result = getRuleOptionsSchema(rule); - - assert.deepStrictEqual(result, noOptionsSchema); - }); - }); -}); From f791da040189ada1b1ec15856557b939ffcd978b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maria=20Jos=C3=A9=20Solano?= Date: Wed, 14 May 2025 00:18:57 -0500 Subject: [PATCH 24/36] chore: remove unbalanced curly brace from `.editorconfig` (#19730) fix: Remove unbalanced curly brace from `.editorconfig` --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 6408cdf80246..a1434423c3e5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,7 +14,7 @@ indent_size = 2 [docs/rules/linebreak-style.md] end_of_line = disabled -[{docs/rules/{indent.md,no-mixed-spaces-and-tabs.md}] +[docs/rules/{indent.md,no-mixed-spaces-and-tabs.md}] indent_style = disabled indent_size = disabled From 4d0c60d0738cb32c12e4ea132caa6fab6d5ed0a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maria=20Jos=C3=A9=20Solano?= Date: Wed, 14 May 2025 01:47:45 -0500 Subject: [PATCH 25/36] docs: Add Neovim to editor integrations (#19729) --- docs/src/use/integrations.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/use/integrations.md b/docs/src/use/integrations.md index b050c906c3f1..548dc0e5194d 100644 --- a/docs/src/use/integrations.md +++ b/docs/src/use/integrations.md @@ -19,6 +19,9 @@ If you would like to recommend an integration to be added to this page, [submit - Vim: - [ALE](https://github.com/dense-analysis/ale) - [Syntastic](https://github.com/vim-syntastic/syntastic/tree/master/syntax_checkers/javascript) +- Neovim: + - [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md#eslint) + - [nvim-lint](https://github.com/mfussenegger/nvim-lint) - Emacs: [Flycheck](http://www.flycheck.org/) supports ESLint with the [javascript-eslint](http://www.flycheck.org/en/latest/languages.html#javascript) checker. - Eclipse Orion: ESLint is the [default linter](https://dev.eclipse.org/mhonarc/lists/orion-dev/msg02718.html) - Eclipse IDE: [Tern ESLint linter](https://github.com/angelozerr/tern.java/wiki/Tern-Linter-ESLint) From dc5ed337fd18cb59801e4afaf394f6b84057b601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Wed, 14 May 2025 23:50:05 +0900 Subject: [PATCH 26/36] fix: correct types and tighten type definitions in `SourceCode` class (#19731) --- lib/languages/js/source-code/source-code.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/languages/js/source-code/source-code.js b/lib/languages/js/source-code/source-code.js index acd9efdfb8cc..65ce4d0957f3 100644 --- a/lib/languages/js/source-code/source-code.js +++ b/lib/languages/js/source-code/source-code.js @@ -53,7 +53,7 @@ const CODE_PATH_EVENTS = [ /** * Validates that the given AST has the required information. * @param {ASTNode} ast The Program node of the AST to check. - * @throws {Error} If the AST doesn't contain the correct information. + * @throws {TypeError} If the AST doesn't contain the correct information. * @returns {void} * @private */ @@ -147,8 +147,8 @@ function sortedMerge(tokens, comments) { * Normalizes a value for a global in a config * @param {(boolean|string|null)} configuredValue The value given for a global in configuration or in * a global directive comment - * @returns {("readable"|"writeable"|"off")} The value normalized as a string - * @throws Error if global value is invalid + * @returns {("readonly"|"writable"|"off")} The value normalized as a string + * @throws {Error} if global value is invalid */ function normalizeConfigGlobal(configuredValue) { switch (configuredValue) { @@ -471,6 +471,10 @@ class SourceCode extends TokenStore { * @type {string[]} */ this.lines = []; + + /** + * @type {number[]} + */ this.lineStartIndices = [0]; const lineEndingPattern = astUtils.createGlobalLinebreakMatcher(); @@ -528,7 +532,7 @@ class SourceCode extends TokenStore { /** * Gets the entire source text split into an array of lines. - * @returns {Array} The source text as an array of lines. + * @returns {string[]} The source text as an array of lines. * @public */ getLines() { @@ -687,8 +691,8 @@ class SourceCode extends TokenStore { /** * Converts a source text index into a (line, column) pair. * @param {number} index The index of a character in a file - * @throws {TypeError} If non-numeric index or index out of range. - * @returns {Object} A {line, column} location object with a 0-indexed column + * @throws {TypeError|RangeError} If non-numeric index or index out of range. + * @returns {{line: number, column: number}} A {line, column} location object with a 0-indexed column * @public */ getLocFromIndex(index) { From ba456e000e104fd7f2dbd27eebbd4f35e6c18934 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 15 May 2025 05:31:14 -0400 Subject: [PATCH 27/36] feat: Externalize MCP server (#19699) * feat: Move MCP server to @eslint/mcp fixes #19682 * Remove unused dependencies * Update docs * Update docs/src/use/mcp.md Co-authored-by: Milos Djermanovic * Install @eslint/mcp --------- Co-authored-by: Milos Djermanovic --- bin/eslint.js | 18 ++-- docs/src/use/mcp.md | 8 +- lib/mcp/mcp-server.js | 66 ------------- package.json | 4 +- tests/bin/eslint.js | 2 +- tests/lib/mcp/mcp-server.js | 189 ------------------------------------ 6 files changed, 13 insertions(+), 274 deletions(-) delete mode 100644 lib/mcp/mcp-server.js delete mode 100644 tests/lib/mcp/mcp-server.js diff --git a/bin/eslint.js b/bin/eslint.js index 7f979557ee82..9b202a982331 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -157,19 +157,15 @@ ${getErrorMessage(error)}`; // start the MCP server if `--mcp` is present if (process.argv.includes("--mcp")) { - const { mcpServer } = require("../lib/mcp/mcp-server"); - const { - StdioServerTransport, - } = require("@modelcontextprotocol/sdk/server/stdio.js"); - - await mcpServer.connect(new StdioServerTransport()); + console.warn( + "You can also run this command directly using 'npx @eslint/mcp@latest'.", + ); - // Note: do not use console.log() because stdout is part of the server transport - console.error(`ESLint MCP server is running. cwd: ${process.cwd()}`); + const spawn = require("cross-spawn"); - process.on("SIGINT", () => { - mcpServer.close(); - process.exitCode = 0; + spawn.sync("npx", ["@eslint/mcp@latest"], { + encoding: "utf8", + stdio: "inherit", }); return; } diff --git a/docs/src/use/mcp.md b/docs/src/use/mcp.md index ed20c9ea63c8..ece841cff7c2 100644 --- a/docs/src/use/mcp.md +++ b/docs/src/use/mcp.md @@ -23,7 +23,7 @@ Create a `.vscode/mcp.json` file in your project with the following configuratio "ESLint": { "type": "stdio", "command": "npx", - "args": ["eslint", "--mcp"] + "args": ["@eslint/mcp@latest"] } } } @@ -34,7 +34,7 @@ Alternatively, you can use the Command Palette: 1. Press `Ctrl+Shift+P` (Windows/Linux) or `Cmd+Shift+P` (macOS) 2. Type and select `MCP: Add Server` 3. Select `Command (stdio)` from the dropdown -4. Enter `npx eslint --mcp` as the command +4. Enter `npx @eslint/mcp@latest` as the command 5. Type `ESLint` as the server ID 6. Choose `Workspace Settings` to create the configuration in `.vscode/mcp.json` @@ -76,7 +76,7 @@ Create a `.cursor/mcp.json` file in your project directory with the following co "mcpServers": { "eslint": { "command": "npx", - "args": ["eslint", "--mcp"], + "args": ["@eslint/mcp@latest"], "env": {} } } @@ -112,7 +112,7 @@ Add the following configuration to your `~/.codeium/windsurf/mcp_config.json` fi "mcpServers": { "eslint": { "command": "npx", - "args": ["eslint", "--mcp"], + "args": ["@eslint/mcp@latest"], "env": {} } } diff --git a/lib/mcp/mcp-server.js b/lib/mcp/mcp-server.js deleted file mode 100644 index 47d5a7dc3d48..000000000000 --- a/lib/mcp/mcp-server.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @fileoverview MCP Server for handling requests and responses to ESLint. - * @author Nicholas C. Zakas - */ - -"use strict"; - -//----------------------------------------------------------------------------- -// Requirements -//----------------------------------------------------------------------------- - -const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js"); -const { z } = require("zod"); -const { ESLint } = require("../eslint"); -const pkg = require("../../package.json"); - -//----------------------------------------------------------------------------- -// Server -//----------------------------------------------------------------------------- - -const mcpServer = new McpServer({ - name: "ESLint", - version: pkg.version, -}); - -// Important: Cursor throws an error when `describe()` is used in the schema. -const filePathsSchema = { - filePaths: z.array(z.string().min(1)).nonempty(), -}; - -//----------------------------------------------------------------------------- -// Tools -//----------------------------------------------------------------------------- - -mcpServer.tool( - "lint-files", - "Lint files using ESLint. You must provide a list of absolute file paths to the files you want to lint. The absolute file paths should be in the correct format for your operating system (e.g., forward slashes on Unix-like systems, backslashes on Windows).", - filePathsSchema, - async ({ filePaths }) => { - const eslint = new ESLint({ - // enable lookup from file rather than from cwd - flags: ["unstable_config_lookup_from_file"], - }); - - const results = await eslint.lintFiles(filePaths); - const content = results.map(result => ({ - type: "text", - text: JSON.stringify(result), - })); - - content.unshift({ - type: "text", - text: "Here are the results of running ESLint on the provided files:", - }); - content.push({ - type: "text", - text: "Do not automatically fix these issues. You must ask the user for confirmation before attempting to fix the issues found.", - }); - - return { - content, - }; - }, -); - -module.exports = { mcpServer }; diff --git a/package.json b/package.json index 1509eaa597f5..92d1086d02e4 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,6 @@ "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", - "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -139,8 +138,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "zod": "^3.24.2" + "optionator": "^0.9.3" }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.0", diff --git a/tests/bin/eslint.js b/tests/bin/eslint.js index c974e7d1723c..ae9412af2f18 100644 --- a/tests/bin/eslint.js +++ b/tests/bin/eslint.js @@ -1241,7 +1241,7 @@ describe("bin/eslint.js", () => { }); child.stderr.on("data", data => { - assert.match(data.toString(), /ESLint MCP server is running/u); + assert.match(data.toString(), /@eslint\/mcp/u); done(); }); }); diff --git a/tests/lib/mcp/mcp-server.js b/tests/lib/mcp/mcp-server.js deleted file mode 100644 index ee5e7b6de2c4..000000000000 --- a/tests/lib/mcp/mcp-server.js +++ /dev/null @@ -1,189 +0,0 @@ -/** - * @fileoverview Tests for MCP server - * @author Nicholas C. Zakas - */ - -"use strict"; - -//----------------------------------------------------------------------------- -// Requirements -//----------------------------------------------------------------------------- - -const { mcpServer } = require("../../../lib/mcp/mcp-server.js"); -const assert = require("chai").assert; -const path = require("node:path"); -const { Client } = require("@modelcontextprotocol/sdk/client/index.js"); -const { InMemoryTransport } = require("@modelcontextprotocol/sdk/inMemory.js"); -const sinon = require("sinon"); - -//----------------------------------------------------------------------------- -// Helpers -//----------------------------------------------------------------------------- - -const filePathsJsonSchema = { - $schema: "http://json-schema.org/draft-07/schema#", - additionalProperties: false, - properties: { - filePaths: { - items: { - type: "string", - minLength: 1, - }, - minItems: 1, - type: "array", - }, - }, - required: ["filePaths"], - type: "object", -}; - -//----------------------------------------------------------------------------- -// Tests -//----------------------------------------------------------------------------- - -describe("MCP Server", () => { - let client, clientTransport, serverTransport; - - beforeEach(async () => { - client = new Client({ - name: "test client", - version: "1.0", - }); - - [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - - // Note: must connect server first or else client hangs - await mcpServer.connect(serverTransport); - await client.connect(clientTransport); - - sinon - .stub(process, "emitWarning") - .callThrough() - .withArgs(sinon.match.any, "ESLintIgnoreWarning") - .returns(); - }); - - afterEach(() => { - sinon.restore(); - }); - - describe("Tools", () => { - it("should list tools", async () => { - const { tools } = await client.listTools(); - - assert.strictEqual(tools.length, 1); - assert.strictEqual(tools[0].name, "lint-files"); - assert.deepStrictEqual(tools[0].inputSchema, filePathsJsonSchema); - }); - - describe("lint-files", () => { - it("should return zero lint messages for a valid file", async () => { - const { content: rawResults } = await client.callTool({ - name: "lint-files", - arguments: { - filePaths: ["tests/fixtures/passing.js"], - }, - }); - - const expectedFilePath = path.join( - process.cwd(), - "tests/fixtures/passing.js", - ); - const results = rawResults - .slice(1, rawResults.length - 1) - .map(({ type, text }) => ({ - type, - text: JSON.parse(text), - })); - - assert.deepStrictEqual(results, [ - { - type: "text", - text: { - filePath: expectedFilePath, - messages: [], - suppressedMessages: [], - errorCount: 0, - fatalErrorCount: 0, - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0, - usedDeprecatedRules: [], - }, - }, - ]); - }); - - it("should return zero lint messages for a valid file and a syntax error for an invalid file", async () => { - const { content: rawResults } = await client.callTool({ - name: "lint-files", - arguments: { - filePaths: [ - "tests/fixtures/passing.js", - "tests/fixtures/syntax-error.js", - ], - }, - }); - - const expectedPassingFilePath = path.join( - process.cwd(), - "tests/fixtures/passing.js", - ); - const expectedFailingFilePath = path.join( - process.cwd(), - "tests/fixtures/syntax-error.js", - ); - - const results = rawResults - .slice(1, rawResults.length - 1) - .map(({ type, text }) => ({ - type, - text: JSON.parse(text), - })); - assert.deepStrictEqual(results, [ - { - type: "text", - text: { - filePath: expectedPassingFilePath, - messages: [], - suppressedMessages: [], - errorCount: 0, - fatalErrorCount: 0, - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0, - usedDeprecatedRules: [], - }, - }, - { - type: "text", - text: { - filePath: expectedFailingFilePath, - messages: [ - { - ruleId: null, - severity: 2, - fatal: true, - message: - "Parsing error: Unexpected token }", - line: 1, - column: 3, - nodeType: null, - }, - ], - suppressedMessages: [], - errorCount: 1, - fatalErrorCount: 1, - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0, - usedDeprecatedRules: [], - source: "{}}\n", - }, - }, - ]); - }); - }); - }); -}); From 596fdc62047dff863e990c3246b32da97ae9a14e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 12:15:44 +0200 Subject: [PATCH 28/36] chore: update dependency @arethetypeswrong/cli to ^0.18.0 (#19732) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/eslint-config-eslint/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 92d1086d02e4..2687b9895b5f 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "optionator": "^0.9.3" }, "devDependencies": { - "@arethetypeswrong/cli": "^0.17.0", + "@arethetypeswrong/cli": "^0.18.0", "@babel/core": "^7.4.3", "@babel/preset-env": "^7.4.3", "@cypress/webpack-preprocessor": "^6.0.2", diff --git a/packages/eslint-config-eslint/package.json b/packages/eslint-config-eslint/package.json index f753bffb720c..79fa33d21036 100644 --- a/packages/eslint-config-eslint/package.json +++ b/packages/eslint-config-eslint/package.json @@ -69,7 +69,7 @@ "eslint-plugin-unicorn": "^52.0.0" }, "devDependencies": { - "@arethetypeswrong/cli": "^0.17.0", + "@arethetypeswrong/cli": "^0.18.0", "eslint": "^9.16.0", "typescript": "^5.7.2" }, From 5687ce7055d30e2d5ef800b3d5c3096c3fc42c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A3=A8=EB=B0=80LuMir?= Date: Fri, 16 May 2025 00:33:14 +0900 Subject: [PATCH 29/36] fix: correct mismatched removed rules (#19734) --- conf/rule-type-list.json | 3 ++- docs/src/_data/rules.json | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/conf/rule-type-list.json b/conf/rule-type-list.json index 03a757808f20..05ddd3cb02af 100644 --- a/conf/rule-type-list.json +++ b/conf/rule-type-list.json @@ -69,7 +69,8 @@ "removed": "space-in-brackets", "replacedBy": [ { "rule": { "name": "object-curly-spacing" } }, - { "rule": { "name": "array-bracket-spacing" } } + { "rule": { "name": "array-bracket-spacing" } }, + { "rule": { "name": "computed-property-spacing" } } ] }, { diff --git a/docs/src/_data/rules.json b/docs/src/_data/rules.json index 47a19b103c00..ae3814f965c8 100644 --- a/docs/src/_data/rules.json +++ b/docs/src/_data/rules.json @@ -3445,6 +3445,11 @@ "rule": { "name": "array-bracket-spacing" } + }, + { + "rule": { + "name": "computed-property-spacing" + } } ] }, From d71e37f450f4ae115ec394615e21523685f0d370 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 15 May 2025 14:16:51 -0400 Subject: [PATCH 30/36] feat: Allow flags to be set in ESLINT_FLAGS env variable (#19717) * feat: Allow flags to be set in ESLINT_FLAGS env variable fixes #19100 * Move environment variable reading into ESLint class * Update docs/src/pages/flags.md Co-authored-by: Milos Djermanovic * Update tests/lib/eslint/eslint.js Co-authored-by: Milos Djermanovic * Update tests/lib/eslint/eslint.js Co-authored-by: Milos Djermanovic * Clarify flags are merged * clean up formatting * Trim environment variable before split --------- Co-authored-by: Milos Djermanovic --- docs/src/pages/flags.md | 14 ++++++ lib/eslint/eslint.js | 16 ++++++- lib/shared/flags.js | 1 + tests/lib/cli.js | 97 ++++++++++++++++++++++++++++++++++++++ tests/lib/eslint/eslint.js | 63 +++++++++++++++++++++++++ 5 files changed, 190 insertions(+), 1 deletion(-) diff --git a/docs/src/pages/flags.md b/docs/src/pages/flags.md index 4d12207180af..0d873fac6bf5 100644 --- a/docs/src/pages/flags.md +++ b/docs/src/pages/flags.md @@ -82,6 +82,16 @@ On the command line, you can specify feature flags using the `--flag` option. Yo args: ["--flag", "flag_one", "--flag", "flag_two", "file.js"] }) }} +### Enable Feature Flags with Environment Variables + +You can also set feature flags using the `ESLINT_FLAGS` environment variable. Multiple flags can be specified as a comma-separated list and are merged with any flags passed on the CLI or in the API. For example, here's how you can add feature flags to your `.bashrc` or `.bash_profile` files: + +```bash +export ESLINT_FLAGS="flag_one,flag_two" +``` + +This approach is especially useful in CI/CD pipelines or when you want to enable the same flags across multiple ESLint commands. + ### Enable Feature Flags with the API When using the API, you can pass a `flags` array to both the `ESLint` and `Linter` classes: @@ -98,6 +108,10 @@ const linter = new Linter({ }); ``` +::: tip +The `ESLint` class also reads the `ESLINT_FLAGS` environment variable to set flags. +::: + ### Enable Feature Flags in VS Code To enable flags in the VS Code ESLint Extension for the editor, specify the flags you'd like in the `eslint.options` setting in your `settings.json` file: diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index d7a94773869a..94c9cbe2dc9d 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -390,6 +390,20 @@ function getFixerForFixTypes(fix, fixTypesSet, config) { originalFix(message); } +/** + * Retrieves flags from the environment variable ESLINT_FLAGS. + * @param {string[]} flags The flags defined via the API. + * @returns {string[]} The merged flags to use. + */ +function mergeEnvironmentFlags(flags) { + if (!process.env.ESLINT_FLAGS) { + return flags; + } + + const envFlags = process.env.ESLINT_FLAGS.trim().split(/\s*,\s*/gu); + return Array.from(new Set([...envFlags, ...flags])); +} + //----------------------------------------------------------------------------- // Main API //----------------------------------------------------------------------------- @@ -420,7 +434,7 @@ class ESLint { const linter = new Linter({ cwd: processedOptions.cwd, configType: "flat", - flags: processedOptions.flags, + flags: mergeEnvironmentFlags(processedOptions.flags), }); const cacheFilePath = getCacheFile( diff --git a/lib/shared/flags.js b/lib/shared/flags.js index ae0e3fb81963..526cf910afad 100644 --- a/lib/shared/flags.js +++ b/lib/shared/flags.js @@ -27,6 +27,7 @@ */ const activeFlags = new Map([ ["test_only", "Used only for testing."], + ["test_only_2", "Used only for testing."], [ "unstable_config_lookup_from_file", "Look up `eslint.config.js` from the file being linted.", diff --git a/tests/lib/cli.js b/tests/lib/cli.js index fd4d5bcc25d5..d560402a0d36 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -2741,6 +2741,11 @@ describe("cli", () => { .returns(); }); + afterEach(() => { + sinon.restore(); + delete process.env.ESLINT_FLAGS; + }); + it("should throw an error when an inactive flag whose feature has been abandoned is used", async () => { const configPath = getFixturePath("eslint.config.js"); const filePath = getFixturePath("passing.js"); @@ -2751,6 +2756,18 @@ describe("cli", () => { }, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned\./u); }); + it("should throw an error when an inactive flag whose feature has been abandoned is used in an environment variable", async () => { + const configPath = getFixturePath("eslint.config.js"); + const filePath = getFixturePath("passing.js"); + + process.env.ESLINT_FLAGS = "test_only_abandoned"; + const input = `--config ${configPath} ${filePath}`; + + await stdAssert.rejects(async () => { + await cli.execute(input, null, true); + }, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned\./u); + }); + it("should error out when an unknown flag is used", async () => { const configPath = getFixturePath("eslint.config.js"); const filePath = getFixturePath("passing.js"); @@ -2761,6 +2778,18 @@ describe("cli", () => { }, /Unknown flag 'test_only_oldx'\./u); }); + it("should error out when an unknown flag is used in an environment variable", async () => { + const configPath = getFixturePath("eslint.config.js"); + const filePath = getFixturePath("passing.js"); + const input = `--config ${configPath} ${filePath}`; + + process.env.ESLINT_FLAGS = "test_only_oldx"; + + await stdAssert.rejects(async () => { + await cli.execute(input, null, true); + }, /Unknown flag 'test_only_oldx'\./u); + }); + it("should emit a warning and not error out when an inactive flag that has been replaced by another flag is used", async () => { const configPath = getFixturePath("eslint.config.js"); const filePath = getFixturePath("passing.js"); @@ -2780,6 +2809,28 @@ describe("cli", () => { assert.strictEqual(exitCode, 0); }); + it("should emit a warning and not error out when an inactive flag that has been replaced by another flag is used in an environment variable", async () => { + const configPath = getFixturePath("eslint.config.js"); + const filePath = getFixturePath("passing.js"); + const input = `--config ${configPath} ${filePath}`; + + process.env.ESLINT_FLAGS = "test_only_replaced"; + + const exitCode = await cli.execute(input, null, true); + + assert.strictEqual( + processStub.callCount, + 1, + "calls `process.emitWarning()` for flags once", + ); + assert.deepStrictEqual(processStub.getCall(0).args, [ + "The flag 'test_only_replaced' is inactive: This flag has been renamed 'test_only' to reflect its stabilization. Please use 'test_only' instead.", + "ESLintInactiveFlag_test_only_replaced", + ]); + sinon.assert.notCalled(log.error); + assert.strictEqual(exitCode, 0); + }); + it("should emit a warning and not error out when an inactive flag whose feature is enabled by default is used", async () => { const configPath = getFixturePath("eslint.config.js"); const filePath = getFixturePath("passing.js"); @@ -2799,6 +2850,27 @@ describe("cli", () => { assert.strictEqual(exitCode, 0); }); + it("should emit a warning and not error out when an inactive flag whose feature is enabled by default is used in an environment variable", async () => { + const configPath = getFixturePath("eslint.config.js"); + const filePath = getFixturePath("passing.js"); + const input = `--config ${configPath} ${filePath}`; + + process.env.ESLINT_FLAGS = "test_only_enabled_by_default"; + + const exitCode = await cli.execute(input, null, true); + assert.strictEqual( + processStub.callCount, + 1, + "calls `process.emitWarning()` for flags once", + ); + assert.deepStrictEqual(processStub.getCall(0).args, [ + "The flag 'test_only_enabled_by_default' is inactive: This feature is now enabled by default.", + "ESLintInactiveFlag_test_only_enabled_by_default", + ]); + sinon.assert.notCalled(log.error); + assert.strictEqual(exitCode, 0); + }); + it("should not error when a valid flag is used", async () => { const configPath = getFixturePath("eslint.config.js"); const filePath = getFixturePath("passing.js"); @@ -2808,6 +2880,31 @@ describe("cli", () => { sinon.assert.notCalled(log.error); assert.strictEqual(exitCode, 0); }); + + it("should not error when a valid flag is used in an environment variable", async () => { + const configPath = getFixturePath("eslint.config.js"); + const filePath = getFixturePath("passing.js"); + const input = `--config ${configPath} ${filePath}`; + + process.env.ESLINT_FLAGS = "test_only"; + + const exitCode = await cli.execute(input, null, true); + + sinon.assert.notCalled(log.error); + assert.strictEqual(exitCode, 0); + }); + + it("should error when a valid flag is used in an environment variable with an abandoned flag", async () => { + const configPath = getFixturePath("eslint.config.js"); + const filePath = getFixturePath("passing.js"); + const input = `--config ${configPath} ${filePath}`; + + process.env.ESLINT_FLAGS = "test_only,test_only_abandoned"; + + await stdAssert.rejects(async () => { + await cli.execute(input, null, true); + }, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned\./u); + }); }); describe("--report-unused-inline-configs option", () => { diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 9844e3f7c771..36a3dc99e430 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -428,6 +428,10 @@ describe("ESLint", () => { .returns(); }); + afterEach(() => { + delete process.env.ESLINT_FLAGS; + }); + it("should return true if the flag is present and active", () => { eslint = new ESLint({ cwd: getFixturePath(), @@ -437,6 +441,47 @@ describe("ESLint", () => { assert.strictEqual(eslint.hasFlag("test_only"), true); }); + it("should return true if the flag is present and active with ESLINT_FLAGS", () => { + process.env.ESLINT_FLAGS = "test_only"; + eslint = new ESLint({ + cwd: getFixturePath(), + }); + assert.strictEqual(eslint.hasFlag("test_only"), true); + }); + + it("should merge flags passed through API with flags passed through ESLINT_FLAGS", () => { + process.env.ESLINT_FLAGS = "test_only"; + eslint = new ESLint({ + cwd: getFixturePath(), + flags: ["test_only_2"], + }); + assert.strictEqual(eslint.hasFlag("test_only"), true); + assert.strictEqual(eslint.hasFlag("test_only_2"), true); + }); + + it("should return true for multiple flags in ESLINT_FLAGS if the flag is present and active and one is duplicated in the API", () => { + process.env.ESLINT_FLAGS = "test_only,test_only_2"; + + eslint = new ESLint({ + cwd: getFixturePath(), + flags: ["test_only"], // intentional duplication + }); + + assert.strictEqual(eslint.hasFlag("test_only"), true); + assert.strictEqual(eslint.hasFlag("test_only_2"), true); + }); + + it("should return true for multiple flags in ESLINT_FLAGS if the flag is present and active and there is leading and trailing white space", () => { + process.env.ESLINT_FLAGS = " test_only, test_only_2 "; + + eslint = new ESLint({ + cwd: getFixturePath(), + }); + + assert.strictEqual(eslint.hasFlag("test_only"), true); + assert.strictEqual(eslint.hasFlag("test_only_2"), true); + }); + it("should return true for the replacement flag if an inactive flag that has been replaced is used", () => { eslint = new ESLint({ cwd: getFixturePath(), @@ -485,6 +530,15 @@ describe("ESLint", () => { }, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned/u); }); + it("should throw an error if an inactive flag whose feature has been abandoned is used in ESLINT_FLAGS", () => { + process.env.ESLINT_FLAGS = "test_only_abandoned"; + assert.throws(() => { + eslint = new ESLint({ + cwd: getFixturePath(), + }); + }, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned/u); + }); + it("should throw an error if the flag is unknown", () => { assert.throws(() => { eslint = new ESLint({ @@ -494,6 +548,15 @@ describe("ESLint", () => { }, /Unknown flag 'foo_bar'/u); }); + it("should throw an error if the flag is unknown in ESLINT_FLAGS", () => { + process.env.ESLINT_FLAGS = "foo_bar"; + assert.throws(() => { + eslint = new ESLint({ + cwd: getFixturePath(), + }); + }, /Unknown flag 'foo_bar'/u); + }); + it("should return false if the flag is not present", () => { eslint = new ESLint({ cwd: getFixturePath() }); From bd5def66d1a3f9bad7da3547b5dff6003e67d9d3 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 15 May 2025 14:27:44 -0400 Subject: [PATCH 31/36] docs: Clean up configuration files docs (#19735) * docs: Clean up configuration files docs * Update docs/src/use/configure/configuration-files.md Co-authored-by: Milos Djermanovic --------- Co-authored-by: Milos Djermanovic --- docs/src/use/configure/configuration-files.md | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/src/use/configure/configuration-files.md b/docs/src/use/configure/configuration-files.md index f3a789aadbe5..119a73dbc96c 100644 --- a/docs/src/use/configure/configuration-files.md +++ b/docs/src/use/configure/configuration-files.md @@ -90,14 +90,40 @@ Each configuration object contains all of the information ESLint needs to execut Patterns specified in `files` and `ignores` use [`minimatch`](https://www.npmjs.com/package/minimatch) syntax and are evaluated relative to the location of the `eslint.config.js` file. If using an alternate config file via the `--config` command line option, then all patterns are evaluated relative to the current working directory. ::: -You can use a combination of `files` and `ignores` to determine which files the configuration object should apply to and which not. By default, ESLint lints files that match the patterns `**/*.js`, `**/*.cjs`, and `**/*.mjs`. Those files are always matched unless you explicitly exclude them using [global ignores](#globally-ignoring-files-with-ignores). -Because config objects that don't specify `files` or `ignores` apply to all files that have been matched by any other configuration object, they will apply to all JavaScript files. For example: +You can use a combination of `files` and `ignores` to determine which files the configuration object should apply to and which not. Here's an example: ```js // eslint.config.js import { defineConfig } from "eslint/config"; export default defineConfig([ + // matches all files ending with .js + { + files: ["**/*.js"], + rules: { + semi: "error", + }, + }, + + // matches all files ending with .js except those in __tests + { + files: ["**/*.js"], + ignores: ["__tests/**"], + rules: { + "no-console": "error", + }, + }, +]); +``` + +Configuration objects without `files` or `ignores` are automatically applied to any file that is matched by any other configuration object. For example: + +```js +// eslint.config.js +import { defineConfig } from "eslint/config"; + +export default defineConfig([ + // matches all files because it doesn't specify the `files` or `ignores` key { rules: { semi: "error", @@ -108,6 +134,10 @@ export default defineConfig([ With this configuration, the `semi` rule is enabled for all files that match the default files in ESLint. So if you pass `example.js` to ESLint, the `semi` rule is applied. If you pass a non-JavaScript file, like `example.txt`, the `semi` rule is not applied because there are no other configuration objects that match that filename. (ESLint outputs an error message letting you know that the file was ignored due to missing configuration.) +::: important +By default, ESLint lints files that match the patterns `**/*.js`, `**/*.cjs`, and `**/*.mjs`. Those files are always matched unless you explicitly exclude them using [global ignores](#globally-ignoring-files-with-ignores). +::: + #### Excluding files with `ignores` You can limit which files a configuration object applies to by specifying a combination of `files` and `ignores` patterns. For example, you may want certain rules to apply only to files in your `src` directory: @@ -129,6 +159,7 @@ export default defineConfig([ Here, only the JavaScript files in the `src` directory have the `semi` rule applied. If you run ESLint on files in another directory, this configuration object is skipped. By adding `ignores`, you can also remove some of the files in `src` from this configuration object: ```js +// eslint.config.js import { defineConfig } from "eslint/config"; export default defineConfig([ @@ -145,6 +176,7 @@ export default defineConfig([ This configuration object matches all JavaScript files in the `src` directory except those that end with `.config.js`. You can also use negation patterns in `ignores` to exclude files from the ignore patterns, such as: ```js +// eslint.config.js import { defineConfig } from "eslint/config"; export default defineConfig([ @@ -165,6 +197,7 @@ Non-global `ignores` patterns can only match file names. A pattern like `"dir-to If `ignores` is used without `files` and there are other keys (such as `rules`), then the configuration object applies to all linted files except the ones excluded by `ignores`, for example: ```js +// eslint.config.js import { defineConfig } from "eslint/config"; export default defineConfig([ From 25de55055d420d7c8b794ae5fdaeb67947c613d9 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 15 May 2025 17:51:02 -0400 Subject: [PATCH 32/36] docs: Update description of frozen rules to mention TypeScript (#19736) --- docs/src/contribute/core-rules.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/contribute/core-rules.md b/docs/src/contribute/core-rules.md index a2dbd04b75da..e5dd8991c5a6 100644 --- a/docs/src/contribute/core-rules.md +++ b/docs/src/contribute/core-rules.md @@ -117,6 +117,7 @@ When a rule is frozen, it means: - **Bug fixes**: We will still fix confirmed bugs. - **New ECMAScript features**: We will ensure compatibility with new ECMAScript features, meaning the rule will not break on new syntax. +- **TypeScript support**: We will ensure compatibility with TypeScript syntax, meaning the rule will not break on TypeScript syntax and violations are appropriate for TypeScript. - **New options**: We will **not** add any new options unless an option is the only way to fix a bug or support a newly-added ECMAScript feature. If you find that a frozen rule would work better for you with a change, we recommend copying the rule source code and modifying it to fit your needs. From ecaef7351f9f3220aa57409bf98db3e55b07a02a Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 16 May 2025 18:28:02 +0000 Subject: [PATCH 33/36] chore: package.json update for @eslint/js release --- packages/js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/package.json b/packages/js/package.json index c535438f8cf1..9563454818a4 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/js", - "version": "9.26.0", + "version": "9.27.0", "description": "ESLint JavaScript language implementation", "funding": "https://eslint.org/donate", "main": "./src/index.js", From f8f1560de633aaf24a7099f89cbbfed12a762a32 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 16 May 2025 20:39:53 +0200 Subject: [PATCH 34/36] chore: upgrade @eslint/js@9.27.0 (#19739) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2687b9895b5f..8e1a969f6d86 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.26.0", + "@eslint/js": "9.27.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", From b7a5c66129c6e504368d1fc452f58c538e4d48e6 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 16 May 2025 18:53:26 +0000 Subject: [PATCH 35/36] Build: changelog update for 9.27.0 --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef83cc19d242..3483b4b6d23b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +v9.27.0 - May 16, 2025 + +* [`f8f1560`](https://github.com/eslint/eslint/commit/f8f1560de633aaf24a7099f89cbbfed12a762a32) chore: upgrade @eslint/js@9.27.0 (#19739) (Milos Djermanovic) +* [`ecaef73`](https://github.com/eslint/eslint/commit/ecaef7351f9f3220aa57409bf98db3e55b07a02a) chore: package.json update for @eslint/js release (Jenkins) +* [`25de550`](https://github.com/eslint/eslint/commit/25de55055d420d7c8b794ae5fdaeb67947c613d9) docs: Update description of frozen rules to mention TypeScript (#19736) (Nicholas C. Zakas) +* [`bd5def6`](https://github.com/eslint/eslint/commit/bd5def66d1a3f9bad7da3547b5dff6003e67d9d3) docs: Clean up configuration files docs (#19735) (Nicholas C. Zakas) +* [`d71e37f`](https://github.com/eslint/eslint/commit/d71e37f450f4ae115ec394615e21523685f0d370) feat: Allow flags to be set in ESLINT_FLAGS env variable (#19717) (Nicholas C. Zakas) +* [`5687ce7`](https://github.com/eslint/eslint/commit/5687ce7055d30e2d5ef800b3d5c3096c3fc42c0e) fix: correct mismatched removed rules (#19734) (루밀LuMir) +* [`596fdc6`](https://github.com/eslint/eslint/commit/596fdc62047dff863e990c3246b32da97ae9a14e) chore: update dependency @arethetypeswrong/cli to ^0.18.0 (#19732) (renovate[bot]) +* [`ba456e0`](https://github.com/eslint/eslint/commit/ba456e000e104fd7f2dbd27eebbd4f35e6c18934) feat: Externalize MCP server (#19699) (Nicholas C. Zakas) +* [`dc5ed33`](https://github.com/eslint/eslint/commit/dc5ed337fd18cb59801e4afaf394f6b84057b601) fix: correct types and tighten type definitions in `SourceCode` class (#19731) (루밀LuMir) +* [`4d0c60d`](https://github.com/eslint/eslint/commit/4d0c60d0738cb32c12e4ea132caa6fab6d5ed0a7) docs: Add Neovim to editor integrations (#19729) (Maria José Solano) +* [`f791da0`](https://github.com/eslint/eslint/commit/f791da040189ada1b1ec15856557b939ffcd978b) chore: remove unbalanced curly brace from `.editorconfig` (#19730) (Maria José Solano) +* [`e86edee`](https://github.com/eslint/eslint/commit/e86edee0918107e4e41e908fe59c937b83f00d4e) refactor: Consolidate Config helpers (#19675) (Nicholas C. Zakas) +* [`07c1a7e`](https://github.com/eslint/eslint/commit/07c1a7e839ec61bd706c651428606ea5955b2bb0) feat: add `allowRegexCharacters` to `no-useless-escape` (#19705) (sethamus) +* [`cf36352`](https://github.com/eslint/eslint/commit/cf3635299e09570b7472286f25dacd8ab24e0517) chore: remove shared types (#19718) (Francesco Trotta) +* [`f60f276`](https://github.com/eslint/eslint/commit/f60f2764971a33e252be13e560dccf21f554dbf1) refactor: Easier RuleContext creation (#19709) (Nicholas C. Zakas) +* [`71317eb`](https://github.com/eslint/eslint/commit/71317ebeaf1c542114e4fcda99ee26115d8e4a27) docs: Update README (GitHub Actions Bot) +* [`de1b5de`](https://github.com/eslint/eslint/commit/de1b5deba069f770140f3a7dba2702c1016dcc2a) fix: correct `service` property name in `Linter.ESLintParseResult` type (#19713) (Francesco Trotta) +* [`58a171e`](https://github.com/eslint/eslint/commit/58a171e8f0dcc1e599ac22bf8c386abacdbee424) chore: update dependency @eslint/plugin-kit to ^0.3.1 (#19712) (renovate[bot]) +* [`3a075a2`](https://github.com/eslint/eslint/commit/3a075a29cfb43ef08711c2e433fb6f218855886d) chore: update dependency @eslint/core to ^0.14.0 (#19715) (renovate[bot]) +* [`60c3e2c`](https://github.com/eslint/eslint/commit/60c3e2cf9256f3676b7934e26ff178aaf19c9e97) fix: sort keys in eslint-suppressions.json to avoid git churn (#19711) (Ron Waldon-Howe) +* [`4c289e6`](https://github.com/eslint/eslint/commit/4c289e685e6cf87331f4b1e6afe34a4feb8e6cc8) docs: Update README (GitHub Actions Bot) +* [`9da90ca`](https://github.com/eslint/eslint/commit/9da90ca3c163adb23a9cc52421f59dedfce34fc9) fix: add `allowReserved` to `Linter.ParserOptions` type (#19710) (Francesco Trotta) +* [`7bc6c71`](https://github.com/eslint/eslint/commit/7bc6c71ca350fa37531291e1d704be6ed408c5dc) feat: add no-unassigned-vars rule (#19618) (Jacob Bandes-Storch) +* [`ee40364`](https://github.com/eslint/eslint/commit/ee4036429758cdaf7f77c52f1c2b74b5a2bb7b66) feat: convert no-array-constructor suggestions to autofixes (#19621) (sethamus) +* [`fbb8be9`](https://github.com/eslint/eslint/commit/fbb8be9256dc7613fa0b87e87974714284b78a94) fix: add `info` to `ESLint.DeprecatedRuleUse` type (#19701) (Francesco Trotta) +* [`f0f0d46`](https://github.com/eslint/eslint/commit/f0f0d46ab2f87e439642abd84b6948b447b66349) docs: clarify that unused suppressions cause non-zero exit code (#19698) (Milos Djermanovic) +* [`44bac9d`](https://github.com/eslint/eslint/commit/44bac9d15c4e0ca099d0b0d85e601f3b55d4e167) ci: run tests in Node.js 24 (#19702) (Francesco Trotta) +* [`32957cd`](https://github.com/eslint/eslint/commit/32957cde72196c7e41741db311786d881c1613a1) feat: support TS syntax in `max-params` (#19557) (Nitin Kumar) +* [`35304dd`](https://github.com/eslint/eslint/commit/35304dd2b0d8a4b640b9a25ae27ebdcb5e124cde) chore: add missing `funding` field to packages (#19684) (루밀LuMir) +* [`8ed3273`](https://github.com/eslint/eslint/commit/8ed32734cc22988173f99fd0703d50f94c60feb8) docs: fix internal usages of `ConfigData` type (#19688) (Francesco Trotta) +* [`f305beb`](https://github.com/eslint/eslint/commit/f305beb82c51215ad48c5c860f02be1b34bcce32) test: mock `process.emitWarning` to prevent output disruption (#19687) (Francesco Trotta) +* [`eb316a8`](https://github.com/eslint/eslint/commit/eb316a83a49347ab47ae965ff95f81dd620d074c) docs: add `fmt` and `check` sections to `Package.json Conventions` (#19686) (루밀LuMir) +* [`a3a2559`](https://github.com/eslint/eslint/commit/a3a255924866b94ef8d604e91636547600edec56) docs: fix wording in Combine Configs (#19685) (Milos Djermanovic) +* [`c8d17e1`](https://github.com/eslint/eslint/commit/c8d17e11dc63909e693eaed5b5ccc50e698ac3b3) docs: Update README (GitHub Actions Bot) + v9.26.0 - May 2, 2025 * [`5b247c8`](https://github.com/eslint/eslint/commit/5b247c859f1b653297a9b9135d92a59742a669cc) chore: upgrade to `@eslint/js@9.26.0` (#19681) (Francesco Trotta) From b9080cf28d88f934941a545a033eb960eceeadbd Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 16 May 2025 18:53:27 +0000 Subject: [PATCH 36/36] 9.27.0 --- docs/package.json | 2 +- docs/src/_data/rule_versions.json | 3 ++- docs/src/_data/rules.json | 2 +- docs/src/_data/rules_meta.json | 11 +++++++++++ docs/src/_data/versions.json | 2 +- docs/src/use/formatters/html-formatter-example.html | 2 +- lib/types/rules.d.ts | 1 + package.json | 2 +- 8 files changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/package.json b/docs/package.json index 9fc63463d42b..ff8817574ab6 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,7 +1,7 @@ { "name": "docs-eslint", "private": true, - "version": "9.26.0", + "version": "9.27.0", "description": "", "main": "index.js", "keywords": [], diff --git a/docs/src/_data/rule_versions.json b/docs/src/_data/rule_versions.json index dbded69dfbae..c497cace7193 100644 --- a/docs/src/_data/rule_versions.json +++ b/docs/src/_data/rule_versions.json @@ -309,7 +309,8 @@ "no-empty-static-block": "8.27.0", "no-new-native-nonconstructor": "8.27.0", "no-object-constructor": "8.50.0", - "no-useless-assignment": "9.0.0-alpha.1" + "no-useless-assignment": "9.0.0-alpha.1", + "no-unassigned-vars": "9.27.0" }, "removed": { "generator-star": "1.0.0-rc-1", diff --git a/docs/src/_data/rules.json b/docs/src/_data/rules.json index ae3814f965c8..bd66c84caa20 100644 --- a/docs/src/_data/rules.json +++ b/docs/src/_data/rules.json @@ -751,7 +751,7 @@ "name": "no-array-constructor", "description": "Disallow `Array` constructors", "recommended": false, - "fixable": false, + "fixable": true, "frozen": false, "hasSuggestions": true }, diff --git a/docs/src/_data/rules_meta.json b/docs/src/_data/rules_meta.json index ef11109190c2..291e85c82370 100644 --- a/docs/src/_data/rules_meta.json +++ b/docs/src/_data/rules_meta.json @@ -1347,6 +1347,11 @@ }, "max-params": { "type": "suggestion", + "dialects": [ + "typescript", + "javascript" + ], + "language": "javascript", "docs": { "description": "Enforce a maximum number of parameters in function definitions", "recommended": false, @@ -1612,6 +1617,7 @@ "recommended": false, "url": "https://eslint.org/docs/latest/rules/no-array-constructor" }, + "fixable": "code", "hasSuggestions": true }, "no-async-promise-executor": { @@ -3552,6 +3558,11 @@ }, "no-useless-escape": { "type": "suggestion", + "defaultOptions": [ + { + "allowRegexCharacters": [] + } + ], "docs": { "description": "Disallow unnecessary escape characters", "recommended": true, diff --git a/docs/src/_data/versions.json b/docs/src/_data/versions.json index 5c985905e075..df69aa53e92d 100644 --- a/docs/src/_data/versions.json +++ b/docs/src/_data/versions.json @@ -6,7 +6,7 @@ "path": "/docs/head/" }, { - "version": "9.26.0", + "version": "9.27.0", "branch": "latest", "path": "/docs/latest/" }, diff --git a/docs/src/use/formatters/html-formatter-example.html b/docs/src/use/formatters/html-formatter-example.html index e657bdc7ab1c..3b75a1d772b1 100644 --- a/docs/src/use/formatters/html-formatter-example.html +++ b/docs/src/use/formatters/html-formatter-example.html @@ -118,7 +118,7 @@

ESLint Report

- 8 problems (4 errors, 4 warnings) - Generated on Fri May 02 2025 21:32:27 GMT+0000 (Coordinated Universal Time) + 8 problems (4 errors, 4 warnings) - Generated on Fri May 16 2025 18:53:28 GMT+0000 (Coordinated Universal Time)
diff --git a/lib/types/rules.d.ts b/lib/types/rules.d.ts index 51844a353f73..974b547155cf 100644 --- a/lib/types/rules.d.ts +++ b/lib/types/rules.d.ts @@ -3717,6 +3717,7 @@ export interface ESLintRules extends Linter.RulesRecord { /** * Rule to disallow `let` or `var` variables that are read but never assigned. * + * @since 9.27.0 * @see https://eslint.org/docs/latest/rules/no-unassigned-vars */ "no-unassigned-vars": Linter.RuleEntry<[]>; diff --git a/package.json b/package.json index 8e1a969f6d86..63d1652ee16e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "9.26.0", + "version": "9.27.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "type": "commonjs",