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
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"
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)
diff --git a/README.md b/README.md
index 19d529d94206..70b434d31574 100644
--- a/README.md
+++ b/README.md
@@ -286,6 +286,11 @@ Josh Goldberg ✨
Tanuj Kanti
+
+
+
+루밀LuMir
+
### Website Team
@@ -320,11 +325,11 @@ 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
-
Platinum Sponsors
+
Platinum Sponsors
Gold Sponsors
Silver Sponsors
Bronze Sponsors
-
+
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.
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/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/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 c3f68d59278c..bd66c84caa20 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",
@@ -743,7 +751,7 @@
"name": "no-array-constructor",
"description": "Disallow `Array` constructors",
"recommended": false,
- "fixable": false,
+ "fixable": true,
"frozen": false,
"hasSuggestions": true
},
@@ -3437,6 +3445,11 @@
"rule": {
"name": "array-bracket-spacing"
}
+ },
+ {
+ "rule": {
+ "name": "computed-property-spacing"
+ }
}
]
},
diff --git a/docs/src/_data/rules_meta.json b/docs/src/_data/rules_meta.json
index 9abf524e8166..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": {
@@ -3260,6 +3266,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": [
@@ -3539,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/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.
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.
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/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/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/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/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/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/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/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
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([
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/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)
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/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
diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js
index 3c9537cceb2f..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").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.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/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/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-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 e4a30c60a8c6..94c9cbe2dc9d 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: {
@@ -57,17 +56,21 @@ 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";
*/
/** @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.
* @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 +82,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.
@@ -159,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) {
@@ -353,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));
}
@@ -387,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
//-----------------------------------------------------------------------------
@@ -417,7 +434,7 @@ class ESLint {
const linter = new Linter({
cwd: processedOptions.cwd,
configType: "flat",
- flags: processedOptions.flags,
+ flags: mergeEnvironmentFlags(processedOptions.flags),
});
const cacheFilePath = getCacheFile(
@@ -611,7 +628,7 @@ class ESLint {
if (!config) {
throw createExtraneousResultsError();
}
- const rule = getRuleFromConfig(ruleId, config);
+ const rule = config.getRuleDefinition(ruleId);
// ignore unknown rules
if (rule) {
@@ -1068,7 +1085,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..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").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.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/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) {
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/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 8b516558ce3d..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;
@@ -75,17 +74,19 @@ 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 */
-/** @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").ESLint.Environment} Environment */
+/** @typedef {import("../types").Linter.GlobalConf} GlobalConf */
/** @typedef {import("../types").Linter.LanguageOptions} JSLanguageOptions */
-/** @typedef {import("../types").Linter.StringSeverity} StringSeverity */
+/** @typedef {import("../types").Linter.LintMessage} LintMessage */
+/** @typedef {import("../types").Linter.Parser} Parser */
+/** @typedef {import("../types").Linter.ParserOptions} ParserOptions */
+/** @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 */
/**
@@ -110,7 +111,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.
*/
@@ -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) {
@@ -1184,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,
@@ -1200,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) {
@@ -1220,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
@@ -1885,7 +1884,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 +1985,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.
@@ -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,
@@ -2348,7 +2341,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.
@@ -2619,7 +2612,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/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/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js
index d2170faf72f5..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");
@@ -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.
@@ -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/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/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/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/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/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/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 47fe84c8dab7..519411d4fbe4 100644
--- a/lib/services/suppressions-service.js
+++ b/lib/services/suppressions-service.js
@@ -12,14 +12,16 @@
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
//------------------------------------------------------------------------------
// 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
@@ -224,7 +226,7 @@ class SuppressionsService {
save(suppressions) {
return fs.promises.writeFile(
this.filePath,
- JSON.stringify(suppressions, null, 2),
+ stringify(suppressions, { space: 2 }),
);
}
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/lib/shared/types.js b/lib/shared/types.js
deleted file mode 100644
index b627d920b4ea..000000000000
--- a/lib/shared/types.js
+++ /dev/null
@@ -1,229 +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} 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.
- * @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 f15b42e038f8..caf9ed811451 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'`:
*
@@ -1619,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;
}
@@ -1648,6 +1683,7 @@ export namespace Linter {
}
interface SuppressedLintMessage extends LintMessage {
+ /** The suppression info. */
suppressions: LintSuppression[];
}
@@ -1661,8 +1697,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;
@@ -1687,9 +1723,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;
}
@@ -1702,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,
@@ -1844,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.
@@ -1857,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;
}
}
@@ -1915,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;
}
@@ -2022,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.
@@ -2057,12 +2153,34 @@ 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;
}
+ /**
+ * Metadata about results for formatters.
+ */
interface ResultsMeta {
+ /**
+ * Present if the maxWarnings threshold was exceeded.
+ */
maxWarningsExceeded?: MaxWarningsExceeded | undefined;
}
diff --git a/lib/types/rules.d.ts b/lib/types/rules.d.ts
index cd3fea23612a..974b547155cf 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,
]
@@ -3710,6 +3714,14 @@ 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<[]>;
+
/**
* Rule to disallow the use of undeclared variables unless mentioned in \/*global *\/ comments.
*
@@ -4130,7 +4142,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/package.json b/package.json
index 99e86a281d86..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",
@@ -108,14 +108,13 @@
"@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",
+ "@eslint/js": "9.27.0",
+ "@eslint/plugin-kit": "^0.3.1",
"@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,11 +138,10 @@
"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",
+ "@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/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/eslint-config-eslint/package.json b/packages/eslint-config-eslint/package.json
index 60c4f675c077..79fa33d21036 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": {
@@ -68,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"
},
diff --git a/packages/js/package.json b/packages/js/package.json
index 4b296314d440..9563454818a4 100644
--- a/packages/js/package.json
+++ b/packages/js/package.json
@@ -1,7 +1,8 @@
{
"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",
"types": "./types/index.d.ts",
"scripts": {
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/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/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/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/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);
- });
- });
-});
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() });
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);
+ });
+ });
+});
diff --git a/tests/lib/mcp/mcp-server.js b/tests/lib/mcp/mcp-server.js
deleted file mode 100644
index f15d1458b75b..000000000000
--- a/tests/lib/mcp/mcp-server.js
+++ /dev/null
@@ -1,178 +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");
-
-//-----------------------------------------------------------------------------
-// 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);
- });
-
- 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",
- },
- },
- ]);
- });
- });
- });
-});
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" }],
+ },
+ ],
+});
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: "[];",
},
],
},
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" },
+ },
+ ],
+ },
+ ],
+});
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`,
+ },
+ ],
+ },
+ ],
+ },
],
});
diff --git a/tests/lib/types/types.test.ts b/tests/lib/types/types.test.ts
index 2c6ef87fd9de..30ffb11c4840 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");
@@ -1073,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,
};
},
@@ -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");
@@ -1774,6 +1794,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";
}
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 7d61728354a1..2e8a5b6cd3da 100644
--- a/tools/eslint-fuzzer.js
+++ b/tools/eslint-fuzzer.js
@@ -16,6 +16,13 @@ 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 */
+/** @typedef {import("../lib/types").Linter.Parser} Parser */
+
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------