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 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/2c8d545/logo.png" alt="AG Grid" height="128"></a></p><h3>Platinum Sponsors</h3>
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
- const { mcpServer } = require("../lib/mcp/mcp-server");
161
- const {
162
- StdioServerTransport,
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
- // Note: do not use console.log() because stdout is part of the server transport
168
- console.error(`ESLint MCP server is running. cwd: ${process.cwd()}`);
164
+ const spawn = require("cross-spawn");
169
165
 
170
- process.on("SIGINT", () => {
171
- mcpServer.close();
172
- process.exitCode = 0;
166
+ spawn.sync("npx", ["@eslint/mcp@latest"], {
167
+ encoding: "utf8",
168
+ stdio: "inherit",
173
169
  });
174
170
  return;
175
171
  }
@@ -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("../shared/types").ConfigData} ConfigData */
62
- /** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */
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
- /** @typedef {import("./eslint/eslint").ESLintOptions} ESLintOptions */
44
- /** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
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("./shared/types").ResultsMeta} ResultsMeta */
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 {FlatConfigArray} [baseConfig] The base config to use.
33
- * @property {Array<FlatConfigObject>} [defaultConfigs] The default configs to use.
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 {FlatConfigObject|Array<FlatConfigObject>} [overrideConfig] The override config to use.
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<ConfigData|undefined>} A configuration object for the file
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<ConfigData|undefined>} A configuration object for the directory
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 {ConfigData|undefined} A configuration object for the file
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 {ConfigData|undefined} A configuration object for the directory
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<ConfigData|undefined>} A configuration object for the file
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 {ConfigData|undefined} A configuration object for the file
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.
@@ -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
- // Helpers
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
- const ruleValidator = new RuleValidator();
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
- ruleValidator.validate(this);
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.