1010//-----------------------------------------------------------------------------
1111
1212const { deepMergeArrays } = require ( "../shared/deep-merge-arrays" ) ;
13- const { getRuleFromConfig } = require ( "./flat-config-helpers" ) ;
1413const { flatConfigSchema, hasMethod } = require ( "./flat-config-schema" ) ;
15- const { RuleValidator } = require ( "./rule-validator" ) ;
1614const { ObjectSchema } = require ( "@eslint/config-array" ) ;
15+ const ajvImport = require ( "../shared/ajv" ) ;
16+ const ajv = ajvImport ( ) ;
17+ const ruleReplacements = require ( "../../conf/replacements.json" ) ;
1718
1819//-----------------------------------------------------------------------------
19- // Helpers
20+ // Typedefs
21+ //-----------------------------------------------------------------------------
22+
23+ /**
24+ * @import { RuleDefinition } from "@eslint/core";
25+ * @import { Linter } from "eslint";
26+ */
27+
2028//-----------------------------------------------------------------------------
29+ // Private Members
30+ //------------------------------------------------------------------------------
2131
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+ } ) ;
2338
2439const severities = new Map ( [
2540 [ 0 , 0 ] ,
@@ -30,6 +45,174 @@ const severities = new Map([
3045 [ "error" , 2 ] ,
3146] ) ;
3247
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+
33216/**
34217 * Splits a plugin identifier in the form a/b/c into two parts: a/b and c.
35218 * @param {string } identifier The identifier to parse.
@@ -124,6 +307,29 @@ function languageOptionsToJSON(languageOptions, objectKey = "languageOptions") {
124307 return result ;
125308}
126309
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+
127333//-----------------------------------------------------------------------------
128334// Exports
129335//-----------------------------------------------------------------------------
@@ -252,7 +458,7 @@ class Config {
252458 // Process the rules
253459 if ( this . rules ) {
254460 this . #normalizeRulesConfig( ) ;
255- ruleValidator . validate ( this ) ;
461+ this . validateRulesConfig ( this . rules ) ;
256462 }
257463 }
258464
@@ -291,6 +497,15 @@ class Config {
291497 } ;
292498 }
293499
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+
294509 /**
295510 * Normalizes the rules configuration. Ensures that each rule config is
296511 * an array and that the severity is a number. Applies meta.defaultOptions.
@@ -323,6 +538,114 @@ class Config {
323538 this . rules [ ruleId ] = ruleConfig ;
324539 }
325540 }
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+ }
326649}
327650
328651module . exports = { Config } ;
0 commit comments