eslint 9.26.0 → 9.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/bin/eslint.js +7 -11
- package/conf/rule-type-list.json +2 -1
- package/lib/cli-engine/cli-engine.js +7 -7
- package/lib/cli.js +5 -4
- package/lib/config/config-loader.js +10 -18
- package/lib/config/config.js +328 -5
- package/lib/eslint/eslint-helpers.js +3 -1
- package/lib/eslint/eslint.js +26 -9
- package/lib/eslint/legacy-eslint.js +6 -6
- package/lib/languages/js/source-code/source-code.js +10 -6
- package/lib/linter/apply-disable-directives.js +1 -1
- package/lib/linter/file-context.js +11 -0
- package/lib/linter/linter.js +75 -82
- package/lib/linter/report-translator.js +2 -1
- package/lib/rule-tester/rule-tester.js +3 -3
- package/lib/rules/index.js +1 -0
- package/lib/rules/max-params.js +32 -7
- package/lib/rules/no-array-constructor.js +51 -1
- package/lib/rules/no-unassigned-vars.js +72 -0
- package/lib/rules/no-useless-escape.js +24 -2
- package/lib/rules/prefer-named-capture-group.js +7 -1
- package/lib/services/processor-service.js +1 -1
- package/lib/services/suppressions-service.js +5 -3
- package/lib/shared/flags.js +1 -0
- package/lib/types/index.d.ts +122 -4
- package/lib/types/rules.d.ts +19 -1
- package/package.json +6 -8
- package/lib/config/flat-config-helpers.js +0 -128
- package/lib/config/rule-validator.js +0 -199
- package/lib/mcp/mcp-server.js +0 -66
- package/lib/shared/types.js +0 -229
package/README.md
CHANGED
@@ -286,6 +286,11 @@ Josh Goldberg ✨
|
|
286
286
|
<img src="https://github.com/Tanujkanti4441.png?s=75" width="75" height="75" alt="Tanuj Kanti's Avatar"><br />
|
287
287
|
Tanuj Kanti
|
288
288
|
</a>
|
289
|
+
</td><td align="center" valign="top" width="11%">
|
290
|
+
<a href="https://github.com/lumirlumir">
|
291
|
+
<img src="https://github.com/lumirlumir.png?s=75" width="75" height="75" alt="루밀LuMir's Avatar"><br />
|
292
|
+
루밀LuMir
|
293
|
+
</a>
|
289
294
|
</td></tr></tbody></table>
|
290
295
|
|
291
296
|
### Website Team
|
@@ -320,11 +325,11 @@ The following companies, organizations, and individuals support ESLint's ongoing
|
|
320
325
|
to get your logo on our READMEs and [website](https://eslint.org/sponsors).
|
321
326
|
|
322
327
|
<h3>Diamond Sponsors</h3>
|
323
|
-
<p><a href="https://www.ag-grid.com/"><img src="https://images.opencollective.com/ag-grid/
|
328
|
+
<p><a href="https://www.ag-grid.com/"><img src="https://images.opencollective.com/ag-grid/bec0580/logo.png" alt="AG Grid" height="128"></a></p><h3>Platinum Sponsors</h3>
|
324
329
|
<p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="128"></a></p><h3>Gold Sponsors</h3>
|
325
330
|
<p><a href="https://qlty.sh/"><img src="https://images.opencollective.com/qltysh/33d157d/logo.png" alt="Qlty Software" height="96"></a> <a href="https://trunk.io/"><img src="https://images.opencollective.com/trunkio/fb92d60/avatar.png" alt="trunk.io" height="96"></a> <a href="https://shopify.engineering/"><img src="https://avatars.githubusercontent.com/u/8085" alt="Shopify" height="96"></a></p><h3>Silver Sponsors</h3>
|
326
331
|
<p><a href="https://vite.dev/"><img src="https://images.opencollective.com/vite/e6d15e1/logo.png" alt="Vite" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301" alt="American Express" height="64"></a> <a href="https://stackblitz.com"><img src="https://avatars.githubusercontent.com/u/28635252" alt="StackBlitz" height="64"></a></p><h3>Bronze Sponsors</h3>
|
327
|
-
<p><a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://nolebase.ayaka.io"><img src="https://avatars.githubusercontent.com/u/11081491" alt="Neko" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a> <a href="https://www.lambdatest.com"><img src="https://avatars.githubusercontent.com/u/171592363" alt="LambdaTest" height="32"></a></p>
|
332
|
+
<p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://nolebase.ayaka.io"><img src="https://avatars.githubusercontent.com/u/11081491" alt="Neko" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a> <a href="https://www.lambdatest.com"><img src="https://avatars.githubusercontent.com/u/171592363" alt="LambdaTest" height="32"></a></p>
|
328
333
|
<h3>Technology Sponsors</h3>
|
329
334
|
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.
|
330
335
|
<p><a href="https://netlify.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/netlify-icon.svg" alt="Netlify" height="32"></a> <a href="https://algolia.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/algolia-icon.svg" alt="Algolia" height="32"></a> <a href="https://1password.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/1password-icon.svg" alt="1Password" height="32"></a></p>
|
package/bin/eslint.js
CHANGED
@@ -157,19 +157,15 @@ ${getErrorMessage(error)}`;
|
|
157
157
|
|
158
158
|
// start the MCP server if `--mcp` is present
|
159
159
|
if (process.argv.includes("--mcp")) {
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
} = require("@modelcontextprotocol/sdk/server/stdio.js");
|
164
|
-
|
165
|
-
await mcpServer.connect(new StdioServerTransport());
|
160
|
+
console.warn(
|
161
|
+
"You can also run this command directly using 'npx @eslint/mcp@latest'.",
|
162
|
+
);
|
166
163
|
|
167
|
-
|
168
|
-
console.error(`ESLint MCP server is running. cwd: ${process.cwd()}`);
|
164
|
+
const spawn = require("cross-spawn");
|
169
165
|
|
170
|
-
|
171
|
-
|
172
|
-
|
166
|
+
spawn.sync("npx", ["@eslint/mcp@latest"], {
|
167
|
+
encoding: "utf8",
|
168
|
+
stdio: "inherit",
|
173
169
|
});
|
174
170
|
return;
|
175
171
|
}
|
package/conf/rule-type-list.json
CHANGED
@@ -69,7 +69,8 @@
|
|
69
69
|
"removed": "space-in-brackets",
|
70
70
|
"replacedBy": [
|
71
71
|
{ "rule": { "name": "object-curly-spacing" } },
|
72
|
-
{ "rule": { "name": "array-bracket-spacing" } }
|
72
|
+
{ "rule": { "name": "array-bracket-spacing" } },
|
73
|
+
{ "rule": { "name": "computed-property-spacing" } }
|
73
74
|
]
|
74
75
|
},
|
75
76
|
{
|
@@ -58,15 +58,15 @@ const validFixTypes = new Set(["directive", "problem", "suggestion", "layout"]);
|
|
58
58
|
//------------------------------------------------------------------------------
|
59
59
|
|
60
60
|
// For VSCode IntelliSense
|
61
|
-
/** @typedef {import("../
|
62
|
-
/** @typedef {import("../
|
63
|
-
/** @typedef {import("../shared/types").LintMessage} LintMessage */
|
64
|
-
/** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */
|
65
|
-
/** @typedef {import("../shared/types").ParserOptions} ParserOptions */
|
66
|
-
/** @typedef {import("../shared/types").RuleConf} RuleConf */
|
67
|
-
/** @typedef {import("../types").Rule.RuleModule} Rule */
|
61
|
+
/** @typedef {import("../types").ESLint.ConfigData} ConfigData */
|
62
|
+
/** @typedef {import("../types").ESLint.DeprecatedRuleUse} DeprecatedRuleInfo */
|
68
63
|
/** @typedef {import("../types").ESLint.FormatterFunction} FormatterFunction */
|
64
|
+
/** @typedef {import("../types").Linter.LintMessage} LintMessage */
|
65
|
+
/** @typedef {import("../types").Linter.ParserOptions} ParserOptions */
|
69
66
|
/** @typedef {import("../types").ESLint.Plugin} Plugin */
|
67
|
+
/** @typedef {import("../types").Rule.RuleModule} Rule */
|
68
|
+
/** @typedef {import("../types").Linter.RuleEntry} RuleConf */
|
69
|
+
/** @typedef {import("../types").Linter.SuppressedLintMessage} SuppressedLintMessage */
|
70
70
|
/** @typedef {ReturnType<CascadingConfigArrayFactory.getConfigArrayForFile>} ConfigArray */
|
71
71
|
/** @typedef {ReturnType<ConfigArray.extractConfig>} ExtractedConfig */
|
72
72
|
|
package/lib/cli.js
CHANGED
@@ -40,12 +40,13 @@ const debug = require("debug")("eslint:cli");
|
|
40
40
|
// Types
|
41
41
|
//------------------------------------------------------------------------------
|
42
42
|
|
43
|
-
/** @
|
44
|
-
|
45
|
-
/** @typedef {import("./eslint/eslint").LintResult} LintResult */
|
43
|
+
/** @import { ESLintOptions } from "./eslint/eslint.js" */
|
44
|
+
|
46
45
|
/** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
|
47
|
-
/** @typedef {import("./
|
46
|
+
/** @typedef {import("./types").Linter.LintMessage} LintMessage */
|
47
|
+
/** @typedef {import("./types").ESLint.LintResult} LintResult */
|
48
48
|
/** @typedef {import("./types").ESLint.Plugin} Plugin */
|
49
|
+
/** @typedef {import("./types").ESLint.ResultsMeta} ResultsMeta */
|
49
50
|
|
50
51
|
//------------------------------------------------------------------------------
|
51
52
|
// Helpers
|
@@ -20,19 +20,17 @@ const { FlatConfigArray } = require("./flat-config-array");
|
|
20
20
|
// Types
|
21
21
|
//-----------------------------------------------------------------------------
|
22
22
|
|
23
|
-
/**
|
24
|
-
* @import { ConfigData, ConfigData as FlatConfigObject } from "../shared/types.js";
|
25
|
-
*/
|
23
|
+
/** @typedef {import("../types").Linter.Config} Config */
|
26
24
|
|
27
25
|
/**
|
28
26
|
* @typedef {Object} ConfigLoaderOptions
|
29
27
|
* @property {string|false|undefined} configFile The path to the config file to use.
|
30
28
|
* @property {string} cwd The current working directory.
|
31
29
|
* @property {boolean} ignoreEnabled Indicates if ignore patterns should be honored.
|
32
|
-
* @property {
|
33
|
-
* @property {Array<
|
30
|
+
* @property {Config|Array<Config>} [baseConfig] The base config to use.
|
31
|
+
* @property {Array<Config>} [defaultConfigs] The default configs to use.
|
34
32
|
* @property {Array<string>} [ignorePatterns] The ignore patterns to use.
|
35
|
-
* @property {
|
33
|
+
* @property {Config|Array<Config>} [overrideConfig] The override config to use.
|
36
34
|
* @property {boolean} [hasUnstableNativeNodeJsTSConfigFlag] The flag to indicate whether the `unstable_native_nodejs_ts_config` flag is enabled.
|
37
35
|
*/
|
38
36
|
|
@@ -394,8 +392,7 @@ class ConfigLoader {
|
|
394
392
|
* This is the same logic used by the ESLint CLI executable to determine
|
395
393
|
* configuration for each file it processes.
|
396
394
|
* @param {string} filePath The path of the file or directory to retrieve config for.
|
397
|
-
* @returns {Promise<
|
398
|
-
* or `undefined` if there is no configuration data for the file.
|
395
|
+
* @returns {Promise<FlatConfigArray>} A configuration object for the file.
|
399
396
|
* @throws {Error} If no configuration for `filePath` exists.
|
400
397
|
*/
|
401
398
|
async loadConfigArrayForFile(filePath) {
|
@@ -415,8 +412,7 @@ class ConfigLoader {
|
|
415
412
|
* This is the same logic used by the ESLint CLI executable to determine
|
416
413
|
* configuration for each file it processes.
|
417
414
|
* @param {string} dirPath The path of the directory to retrieve config for.
|
418
|
-
* @returns {Promise<
|
419
|
-
* or `undefined` if there is no configuration data for the directory.
|
415
|
+
* @returns {Promise<FlatConfigArray>} A configuration object for the directory.
|
420
416
|
*/
|
421
417
|
async loadConfigArrayForDirectory(dirPath) {
|
422
418
|
assertValidFilePath(dirPath);
|
@@ -440,8 +436,7 @@ class ConfigLoader {
|
|
440
436
|
* intended to be used in locations where we know the config file has already
|
441
437
|
* been loaded and we just need to get the configuration for a file.
|
442
438
|
* @param {string} filePath The path of the file to retrieve a config object for.
|
443
|
-
* @returns {
|
444
|
-
* or `undefined` if there is no configuration data for the file.
|
439
|
+
* @returns {FlatConfigArray} A configuration object for the file.
|
445
440
|
* @throws {Error} If `filePath` is not a non-empty string.
|
446
441
|
* @throws {Error} If `filePath` is not an absolute path.
|
447
442
|
* @throws {Error} If the config file was not already loaded.
|
@@ -460,8 +455,7 @@ class ConfigLoader {
|
|
460
455
|
* intended to be used in locations where we know the config file has already
|
461
456
|
* been loaded and we just need to get the configuration for a file.
|
462
457
|
* @param {string} fileOrDirPath The path of the directory to retrieve a config object for.
|
463
|
-
* @returns {
|
464
|
-
* or `undefined` if there is no configuration data for the directory.
|
458
|
+
* @returns {FlatConfigArray} A configuration object for the directory.
|
465
459
|
* @throws {Error} If `dirPath` is not a non-empty string.
|
466
460
|
* @throws {Error} If `dirPath` is not an absolute path.
|
467
461
|
* @throws {Error} If the config file was not already loaded.
|
@@ -789,8 +783,7 @@ class LegacyConfigLoader extends ConfigLoader {
|
|
789
783
|
* This is the same logic used by the ESLint CLI executable to determine
|
790
784
|
* configuration for each file it processes.
|
791
785
|
* @param {string} dirPath The path of the directory to retrieve config for.
|
792
|
-
* @returns {Promise<
|
793
|
-
* or `undefined` if there is no configuration data for the file.
|
786
|
+
* @returns {Promise<FlatConfigArray>} A configuration object for the file.
|
794
787
|
*/
|
795
788
|
async loadConfigArrayForDirectory(dirPath) {
|
796
789
|
assertValidFilePath(dirPath);
|
@@ -812,8 +805,7 @@ class LegacyConfigLoader extends ConfigLoader {
|
|
812
805
|
* intended to be used in locations where we know the config file has already
|
813
806
|
* been loaded and we just need to get the configuration for a file.
|
814
807
|
* @param {string} dirPath The path of the directory to retrieve a config object for.
|
815
|
-
* @returns {
|
816
|
-
* or `undefined` if there is no configuration data for the file.
|
808
|
+
* @returns {FlatConfigArray} A configuration object for the file.
|
817
809
|
* @throws {Error} If `dirPath` is not a non-empty string.
|
818
810
|
* @throws {Error} If `dirPath` is not an absolute path.
|
819
811
|
* @throws {Error} If the config file was not already loaded.
|
package/lib/config/config.js
CHANGED
@@ -10,16 +10,31 @@
|
|
10
10
|
//-----------------------------------------------------------------------------
|
11
11
|
|
12
12
|
const { deepMergeArrays } = require("../shared/deep-merge-arrays");
|
13
|
-
const { getRuleFromConfig } = require("./flat-config-helpers");
|
14
13
|
const { flatConfigSchema, hasMethod } = require("./flat-config-schema");
|
15
|
-
const { RuleValidator } = require("./rule-validator");
|
16
14
|
const { ObjectSchema } = require("@eslint/config-array");
|
15
|
+
const ajvImport = require("../shared/ajv");
|
16
|
+
const ajv = ajvImport();
|
17
|
+
const ruleReplacements = require("../../conf/replacements.json");
|
17
18
|
|
18
19
|
//-----------------------------------------------------------------------------
|
19
|
-
//
|
20
|
+
// Typedefs
|
21
|
+
//-----------------------------------------------------------------------------
|
22
|
+
|
23
|
+
/**
|
24
|
+
* @import { RuleDefinition } from "@eslint/core";
|
25
|
+
* @import { Linter } from "eslint";
|
26
|
+
*/
|
27
|
+
|
20
28
|
//-----------------------------------------------------------------------------
|
29
|
+
// Private Members
|
30
|
+
//------------------------------------------------------------------------------
|
21
31
|
|
22
|
-
|
32
|
+
// JSON schema that disallows passing any options
|
33
|
+
const noOptionsSchema = Object.freeze({
|
34
|
+
type: "array",
|
35
|
+
minItems: 0,
|
36
|
+
maxItems: 0,
|
37
|
+
});
|
23
38
|
|
24
39
|
const severities = new Map([
|
25
40
|
[0, 0],
|
@@ -30,6 +45,174 @@ const severities = new Map([
|
|
30
45
|
["error", 2],
|
31
46
|
]);
|
32
47
|
|
48
|
+
/**
|
49
|
+
* A collection of compiled validators for rules that have already
|
50
|
+
* been validated.
|
51
|
+
* @type {WeakMap}
|
52
|
+
*/
|
53
|
+
const validators = new WeakMap();
|
54
|
+
|
55
|
+
//-----------------------------------------------------------------------------
|
56
|
+
// Helpers
|
57
|
+
//-----------------------------------------------------------------------------
|
58
|
+
|
59
|
+
/**
|
60
|
+
* Throws a helpful error when a rule cannot be found.
|
61
|
+
* @param {Object} ruleId The rule identifier.
|
62
|
+
* @param {string} ruleId.pluginName The ID of the rule to find.
|
63
|
+
* @param {string} ruleId.ruleName The ID of the rule to find.
|
64
|
+
* @param {Object} config The config to search in.
|
65
|
+
* @throws {TypeError} For missing plugin or rule.
|
66
|
+
* @returns {void}
|
67
|
+
*/
|
68
|
+
function throwRuleNotFoundError({ pluginName, ruleName }, config) {
|
69
|
+
const ruleId = pluginName === "@" ? ruleName : `${pluginName}/${ruleName}`;
|
70
|
+
|
71
|
+
const errorMessageHeader = `Key "rules": Key "${ruleId}"`;
|
72
|
+
|
73
|
+
let errorMessage = `${errorMessageHeader}: Could not find plugin "${pluginName}" in configuration.`;
|
74
|
+
|
75
|
+
const missingPluginErrorMessage = errorMessage;
|
76
|
+
|
77
|
+
// if the plugin exists then we need to check if the rule exists
|
78
|
+
if (config.plugins && config.plugins[pluginName]) {
|
79
|
+
const replacementRuleName = ruleReplacements.rules[ruleName];
|
80
|
+
|
81
|
+
if (pluginName === "@" && replacementRuleName) {
|
82
|
+
errorMessage = `${errorMessageHeader}: Rule "${ruleName}" was removed and replaced by "${replacementRuleName}".`;
|
83
|
+
} else {
|
84
|
+
errorMessage = `${errorMessageHeader}: Could not find "${ruleName}" in plugin "${pluginName}".`;
|
85
|
+
|
86
|
+
// otherwise, let's see if we can find the rule name elsewhere
|
87
|
+
for (const [otherPluginName, otherPlugin] of Object.entries(
|
88
|
+
config.plugins,
|
89
|
+
)) {
|
90
|
+
if (otherPlugin.rules && otherPlugin.rules[ruleName]) {
|
91
|
+
errorMessage += ` Did you mean "${otherPluginName}/${ruleName}"?`;
|
92
|
+
break;
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
// falls through to throw error
|
98
|
+
}
|
99
|
+
|
100
|
+
const error = new TypeError(errorMessage);
|
101
|
+
|
102
|
+
if (errorMessage === missingPluginErrorMessage) {
|
103
|
+
error.messageTemplate = "config-plugin-missing";
|
104
|
+
error.messageData = { pluginName, ruleId };
|
105
|
+
}
|
106
|
+
|
107
|
+
throw error;
|
108
|
+
}
|
109
|
+
|
110
|
+
/**
|
111
|
+
* The error type when a rule has an invalid `meta.schema`.
|
112
|
+
*/
|
113
|
+
class InvalidRuleOptionsSchemaError extends Error {
|
114
|
+
/**
|
115
|
+
* Creates a new instance.
|
116
|
+
* @param {string} ruleId Id of the rule that has an invalid `meta.schema`.
|
117
|
+
* @param {Error} processingError Error caught while processing the `meta.schema`.
|
118
|
+
*/
|
119
|
+
constructor(ruleId, processingError) {
|
120
|
+
super(
|
121
|
+
`Error while processing options validation schema of rule '${ruleId}': ${processingError.message}`,
|
122
|
+
{ cause: processingError },
|
123
|
+
);
|
124
|
+
this.code = "ESLINT_INVALID_RULE_OPTIONS_SCHEMA";
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
/**
|
129
|
+
* Parses a ruleId into its plugin and rule parts.
|
130
|
+
* @param {string} ruleId The rule ID to parse.
|
131
|
+
* @returns {{pluginName:string,ruleName:string}} The plugin and rule
|
132
|
+
* parts of the ruleId;
|
133
|
+
*/
|
134
|
+
function parseRuleId(ruleId) {
|
135
|
+
let pluginName, ruleName;
|
136
|
+
|
137
|
+
// distinguish between core rules and plugin rules
|
138
|
+
if (ruleId.includes("/")) {
|
139
|
+
// mimic scoped npm packages
|
140
|
+
if (ruleId.startsWith("@")) {
|
141
|
+
pluginName = ruleId.slice(0, ruleId.lastIndexOf("/"));
|
142
|
+
} else {
|
143
|
+
pluginName = ruleId.slice(0, ruleId.indexOf("/"));
|
144
|
+
}
|
145
|
+
|
146
|
+
ruleName = ruleId.slice(pluginName.length + 1);
|
147
|
+
} else {
|
148
|
+
pluginName = "@";
|
149
|
+
ruleName = ruleId;
|
150
|
+
}
|
151
|
+
|
152
|
+
return {
|
153
|
+
pluginName,
|
154
|
+
ruleName,
|
155
|
+
};
|
156
|
+
}
|
157
|
+
|
158
|
+
/**
|
159
|
+
* Retrieves a rule instance from a given config based on the ruleId.
|
160
|
+
* @param {string} ruleId The rule ID to look for.
|
161
|
+
* @param {Linter.Config} config The config to search.
|
162
|
+
* @returns {RuleDefinition|undefined} The rule if found
|
163
|
+
* or undefined if not.
|
164
|
+
*/
|
165
|
+
function getRuleFromConfig(ruleId, config) {
|
166
|
+
const { pluginName, ruleName } = parseRuleId(ruleId);
|
167
|
+
|
168
|
+
return config.plugins?.[pluginName]?.rules?.[ruleName];
|
169
|
+
}
|
170
|
+
|
171
|
+
/**
|
172
|
+
* Gets a complete options schema for a rule.
|
173
|
+
* @param {RuleDefinition} rule A rule object
|
174
|
+
* @throws {TypeError} If `meta.schema` is specified but is not an array, object or `false`.
|
175
|
+
* @returns {Object|null} JSON Schema for the rule's options. `null` if `meta.schema` is `false`.
|
176
|
+
*/
|
177
|
+
function getRuleOptionsSchema(rule) {
|
178
|
+
if (!rule.meta) {
|
179
|
+
return { ...noOptionsSchema }; // default if `meta.schema` is not specified
|
180
|
+
}
|
181
|
+
|
182
|
+
const schema = rule.meta.schema;
|
183
|
+
|
184
|
+
if (typeof schema === "undefined") {
|
185
|
+
return { ...noOptionsSchema }; // default if `meta.schema` is not specified
|
186
|
+
}
|
187
|
+
|
188
|
+
// `schema:false` is an allowed explicit opt-out of options validation for the rule
|
189
|
+
if (schema === false) {
|
190
|
+
return null;
|
191
|
+
}
|
192
|
+
|
193
|
+
if (typeof schema !== "object" || schema === null) {
|
194
|
+
throw new TypeError("Rule's `meta.schema` must be an array or object");
|
195
|
+
}
|
196
|
+
|
197
|
+
// ESLint-specific array form needs to be converted into a valid JSON Schema definition
|
198
|
+
if (Array.isArray(schema)) {
|
199
|
+
if (schema.length) {
|
200
|
+
return {
|
201
|
+
type: "array",
|
202
|
+
items: schema,
|
203
|
+
minItems: 0,
|
204
|
+
maxItems: schema.length,
|
205
|
+
};
|
206
|
+
}
|
207
|
+
|
208
|
+
// `schema:[]` is an explicit way to specify that the rule does not accept any options
|
209
|
+
return { ...noOptionsSchema };
|
210
|
+
}
|
211
|
+
|
212
|
+
// `schema:<object>` is assumed to be a valid JSON Schema definition
|
213
|
+
return schema;
|
214
|
+
}
|
215
|
+
|
33
216
|
/**
|
34
217
|
* Splits a plugin identifier in the form a/b/c into two parts: a/b and c.
|
35
218
|
* @param {string} identifier The identifier to parse.
|
@@ -124,6 +307,29 @@ function languageOptionsToJSON(languageOptions, objectKey = "languageOptions") {
|
|
124
307
|
return result;
|
125
308
|
}
|
126
309
|
|
310
|
+
/**
|
311
|
+
* Gets or creates a validator for a rule.
|
312
|
+
* @param {Object} rule The rule to get a validator for.
|
313
|
+
* @param {string} ruleId The ID of the rule (for error reporting).
|
314
|
+
* @returns {Function|null} A validation function or null if no validation is needed.
|
315
|
+
* @throws {InvalidRuleOptionsSchemaError} If a rule's `meta.schema` is invalid.
|
316
|
+
*/
|
317
|
+
function getOrCreateValidator(rule, ruleId) {
|
318
|
+
if (!validators.has(rule)) {
|
319
|
+
try {
|
320
|
+
const schema = getRuleOptionsSchema(rule);
|
321
|
+
|
322
|
+
if (schema) {
|
323
|
+
validators.set(rule, ajv.compile(schema));
|
324
|
+
}
|
325
|
+
} catch (err) {
|
326
|
+
throw new InvalidRuleOptionsSchemaError(ruleId, err);
|
327
|
+
}
|
328
|
+
}
|
329
|
+
|
330
|
+
return validators.get(rule);
|
331
|
+
}
|
332
|
+
|
127
333
|
//-----------------------------------------------------------------------------
|
128
334
|
// Exports
|
129
335
|
//-----------------------------------------------------------------------------
|
@@ -252,7 +458,7 @@ class Config {
|
|
252
458
|
// Process the rules
|
253
459
|
if (this.rules) {
|
254
460
|
this.#normalizeRulesConfig();
|
255
|
-
|
461
|
+
this.validateRulesConfig(this.rules);
|
256
462
|
}
|
257
463
|
}
|
258
464
|
|
@@ -291,6 +497,15 @@ class Config {
|
|
291
497
|
};
|
292
498
|
}
|
293
499
|
|
500
|
+
/**
|
501
|
+
* Gets a rule configuration by its ID.
|
502
|
+
* @param {string} ruleId The ID of the rule to get.
|
503
|
+
* @returns {RuleDefinition|undefined} The rule definition from the plugin, or `undefined` if the rule is not found.
|
504
|
+
*/
|
505
|
+
getRuleDefinition(ruleId) {
|
506
|
+
return getRuleFromConfig(ruleId, this);
|
507
|
+
}
|
508
|
+
|
294
509
|
/**
|
295
510
|
* Normalizes the rules configuration. Ensures that each rule config is
|
296
511
|
* an array and that the severity is a number. Applies meta.defaultOptions.
|
@@ -323,6 +538,114 @@ class Config {
|
|
323
538
|
this.rules[ruleId] = ruleConfig;
|
324
539
|
}
|
325
540
|
}
|
541
|
+
|
542
|
+
/**
|
543
|
+
* Validates all of the rule configurations in the given rules config
|
544
|
+
* against the plugins in this instance. This is used primarily to
|
545
|
+
* validate inline configuration rules while inting.
|
546
|
+
* @param {Object} rulesConfig The rules config to validate.
|
547
|
+
* @returns {void}
|
548
|
+
* @throws {Error} If a rule's configuration does not match its schema.
|
549
|
+
* @throws {TypeError} If the rulesConfig is not provided or is invalid.
|
550
|
+
* @throws {InvalidRuleOptionsSchemaError} If a rule's `meta.schema` is invalid.
|
551
|
+
* @throws {TypeError} If a rule is not found in the plugins.
|
552
|
+
*/
|
553
|
+
validateRulesConfig(rulesConfig) {
|
554
|
+
if (!rulesConfig) {
|
555
|
+
throw new TypeError("Config is required for validation.");
|
556
|
+
}
|
557
|
+
|
558
|
+
for (const [ruleId, ruleOptions] of Object.entries(rulesConfig)) {
|
559
|
+
// check for edge case
|
560
|
+
if (ruleId === "__proto__") {
|
561
|
+
continue;
|
562
|
+
}
|
563
|
+
|
564
|
+
/*
|
565
|
+
* If a rule is disabled, we don't do any validation. This allows
|
566
|
+
* users to safely set any value to 0 or "off" without worrying
|
567
|
+
* that it will cause a validation error.
|
568
|
+
*
|
569
|
+
* Note: ruleOptions is always an array at this point because
|
570
|
+
* this validation occurs after FlatConfigArray has merged and
|
571
|
+
* normalized values.
|
572
|
+
*/
|
573
|
+
if (ruleOptions[0] === 0) {
|
574
|
+
continue;
|
575
|
+
}
|
576
|
+
|
577
|
+
const rule = getRuleFromConfig(ruleId, this);
|
578
|
+
|
579
|
+
if (!rule) {
|
580
|
+
throwRuleNotFoundError(parseRuleId(ruleId), this);
|
581
|
+
}
|
582
|
+
|
583
|
+
const validateRule = getOrCreateValidator(rule, ruleId);
|
584
|
+
|
585
|
+
if (validateRule) {
|
586
|
+
validateRule(ruleOptions.slice(1));
|
587
|
+
|
588
|
+
if (validateRule.errors) {
|
589
|
+
throw new Error(
|
590
|
+
`Key "rules": Key "${ruleId}":\n${validateRule.errors
|
591
|
+
.map(error => {
|
592
|
+
if (
|
593
|
+
error.keyword === "additionalProperties" &&
|
594
|
+
error.schema === false &&
|
595
|
+
typeof error.parentSchema?.properties ===
|
596
|
+
"object" &&
|
597
|
+
typeof error.params?.additionalProperty ===
|
598
|
+
"string"
|
599
|
+
) {
|
600
|
+
const expectedProperties = Object.keys(
|
601
|
+
error.parentSchema.properties,
|
602
|
+
).map(property => `"${property}"`);
|
603
|
+
|
604
|
+
return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n\t\tUnexpected property "${error.params.additionalProperty}". Expected properties: ${expectedProperties.join(", ")}.\n`;
|
605
|
+
}
|
606
|
+
|
607
|
+
return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`;
|
608
|
+
})
|
609
|
+
.join("")}`,
|
610
|
+
);
|
611
|
+
}
|
612
|
+
}
|
613
|
+
}
|
614
|
+
}
|
615
|
+
|
616
|
+
/**
|
617
|
+
* Gets a complete options schema for a rule.
|
618
|
+
* @param {RuleDefinition} ruleDefinition A rule definition object.
|
619
|
+
* @throws {TypeError} If `meta.schema` is specified but is not an array, object or `false`.
|
620
|
+
* @returns {Object|null} JSON Schema for the rule's options. `null` if `meta.schema` is `false`.
|
621
|
+
*/
|
622
|
+
static getRuleOptionsSchema(ruleDefinition) {
|
623
|
+
return getRuleOptionsSchema(ruleDefinition);
|
624
|
+
}
|
625
|
+
|
626
|
+
/**
|
627
|
+
* Normalizes the severity value of a rule's configuration to a number
|
628
|
+
* @param {(number|string|[number, ...*]|[string, ...*])} ruleConfig A rule's configuration value, generally
|
629
|
+
* received from the user. A valid config value is either 0, 1, 2, the string "off" (treated the same as 0),
|
630
|
+
* the string "warn" (treated the same as 1), the string "error" (treated the same as 2), or an array
|
631
|
+
* whose first element is one of the above values. Strings are matched case-insensitively.
|
632
|
+
* @returns {(0|1|2)} The numeric severity value if the config value was valid, otherwise 0.
|
633
|
+
*/
|
634
|
+
static getRuleNumericSeverity(ruleConfig) {
|
635
|
+
const severityValue = Array.isArray(ruleConfig)
|
636
|
+
? ruleConfig[0]
|
637
|
+
: ruleConfig;
|
638
|
+
|
639
|
+
if (severities.has(severityValue)) {
|
640
|
+
return severities.get(severityValue);
|
641
|
+
}
|
642
|
+
|
643
|
+
if (typeof severityValue === "string") {
|
644
|
+
return severities.get(severityValue.toLowerCase()) ?? 0;
|
645
|
+
}
|
646
|
+
|
647
|
+
return 0;
|
648
|
+
}
|
326
649
|
}
|
327
650
|
|
328
651
|
module.exports = { Config };
|
@@ -30,10 +30,12 @@ const MINIMATCH_OPTIONS = { dot: true };
|
|
30
30
|
|
31
31
|
/**
|
32
32
|
* @import { ESLintOptions } from "./eslint.js";
|
33
|
-
* @import { LintMessage, LintResult } from "../shared/types.js";
|
34
33
|
* @import { ConfigLoader, LegacyConfigLoader } from "../config/config-loader.js";
|
35
34
|
*/
|
36
35
|
|
36
|
+
/** @typedef {import("../types").Linter.LintMessage} LintMessage */
|
37
|
+
/** @typedef {import("../types").ESLint.LintResult} LintResult */
|
38
|
+
|
37
39
|
/**
|
38
40
|
* @typedef {Object} GlobSearch
|
39
41
|
* @property {Array<string>} patterns The normalized patterns to use for a search.
|