Skip to content

Commit a6c240d

Browse files
authored
feat!: Set default schema: [], drop support for function-style rules (#139)
* drop support for function-style rules * implement schema changes * add unit tests for ConfigValidator#getRuleOptionsSchema * add regression tests for ConfigValidator#validateRuleOptions * add tests for error code * update fixture rules to use object-style
1 parent 01db002 commit a6c240d

File tree

8 files changed

+374
-76
lines changed

8 files changed

+374
-76
lines changed

lib/config-array/config-array.js

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -319,31 +319,18 @@ function createConfig(instance, indices) {
319319
* @param {string} pluginId The plugin ID for prefix.
320320
* @param {Record<string,T>} defs The definitions to collect.
321321
* @param {Map<string, U>} map The map to output.
322-
* @param {function(T): U} [normalize] The normalize function for each value.
323322
* @returns {void}
324323
*/
325-
function collect(pluginId, defs, map, normalize) {
324+
function collect(pluginId, defs, map) {
326325
if (defs) {
327326
const prefix = pluginId && `${pluginId}/`;
328327

329328
for (const [key, value] of Object.entries(defs)) {
330-
map.set(
331-
`${prefix}${key}`,
332-
normalize ? normalize(value) : value
333-
);
329+
map.set(`${prefix}${key}`, value);
334330
}
335331
}
336332
}
337333

338-
/**
339-
* Normalize a rule definition.
340-
* @param {Function|Rule} rule The rule definition to normalize.
341-
* @returns {Rule} The normalized rule definition.
342-
*/
343-
function normalizePluginRule(rule) {
344-
return typeof rule === "function" ? { create: rule } : rule;
345-
}
346-
347334
/**
348335
* Delete the mutation methods from a given map.
349336
* @param {Map<any, any>} map The map object to delete.
@@ -385,7 +372,7 @@ function initPluginMemberMaps(elements, slots) {
385372

386373
collect(pluginId, plugin.environments, slots.envMap);
387374
collect(pluginId, plugin.processors, slots.processorMap);
388-
collect(pluginId, plugin.rules, slots.ruleMap, normalizePluginRule);
375+
collect(pluginId, plugin.rules, slots.ruleMap);
389376
}
390377
}
391378

lib/shared/config-validator.js

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55

66
/* eslint class-methods-use-this: "off" */
77

8+
//------------------------------------------------------------------------------
9+
// Typedefs
10+
//------------------------------------------------------------------------------
11+
12+
/** @typedef {import("../shared/types").Rule} Rule */
13+
814
//------------------------------------------------------------------------------
915
// Requirements
1016
//------------------------------------------------------------------------------
@@ -33,6 +39,13 @@ const severityMap = {
3339

3440
const validated = new WeakSet();
3541

42+
// JSON schema that disallows passing any options
43+
const noOptionsSchema = Object.freeze({
44+
type: "array",
45+
minItems: 0,
46+
maxItems: 0
47+
});
48+
3649
//-----------------------------------------------------------------------------
3750
// Exports
3851
//-----------------------------------------------------------------------------
@@ -44,17 +57,36 @@ export default class ConfigValidator {
4457

4558
/**
4659
* Gets a complete options schema for a rule.
47-
* @param {{create: Function, schema: (Array|null)}} rule A new-style rule object
48-
* @returns {Object} JSON Schema for the rule's options.
60+
* @param {Rule} rule A rule object
61+
* @throws {TypeError} If `meta.schema` is specified but is not an array, object or `false`.
62+
* @returns {Object|null} JSON Schema for the rule's options.
63+
* `null` if rule wasn't passed or its `meta.schema` is `false`.
4964
*/
5065
getRuleOptionsSchema(rule) {
5166
if (!rule) {
5267
return null;
5368
}
5469

55-
const schema = rule.schema || rule.meta && rule.meta.schema;
70+
if (!rule.meta) {
71+
return { ...noOptionsSchema }; // default if `meta.schema` is not specified
72+
}
5673

57-
// Given a tuple of schemas, insert warning level at the beginning
74+
const schema = rule.meta.schema;
75+
76+
if (typeof schema === "undefined") {
77+
return { ...noOptionsSchema }; // default if `meta.schema` is not specified
78+
}
79+
80+
// `schema:false` is an allowed explicit opt-out of options validation for the rule
81+
if (schema === false) {
82+
return null;
83+
}
84+
85+
if (typeof schema !== "object" || schema === null) {
86+
throw new TypeError("Rule's `meta.schema` must be an array or object");
87+
}
88+
89+
// ESLint-specific array form needs to be converted into a valid JSON Schema definition
5890
if (Array.isArray(schema)) {
5991
if (schema.length) {
6092
return {
@@ -64,16 +96,13 @@ export default class ConfigValidator {
6496
maxItems: schema.length
6597
};
6698
}
67-
return {
68-
type: "array",
69-
minItems: 0,
70-
maxItems: 0
71-
};
7299

100+
// `schema:[]` is an explicit way to specify that the rule does not accept any options
101+
return { ...noOptionsSchema };
73102
}
74103

75-
// Given a full schema, leave it alone
76-
return schema || null;
104+
// `schema:<object>` is assumed to be a valid JSON Schema definition
105+
return schema;
77106
}
78107

79108
/**
@@ -101,10 +130,18 @@ export default class ConfigValidator {
101130
*/
102131
validateRuleSchema(rule, localOptions) {
103132
if (!ruleValidators.has(rule)) {
104-
const schema = this.getRuleOptionsSchema(rule);
133+
try {
134+
const schema = this.getRuleOptionsSchema(rule);
135+
136+
if (schema) {
137+
ruleValidators.set(rule, ajv.compile(schema));
138+
}
139+
} catch (err) {
140+
const errorWithCode = new Error(err.message, { cause: err });
105141

106-
if (schema) {
107-
ruleValidators.set(rule, ajv.compile(schema));
142+
errorWithCode.code = "ESLINT_INVALID_RULE_OPTIONS_SCHEMA";
143+
144+
throw errorWithCode;
108145
}
109146
}
110147

@@ -137,13 +174,21 @@ export default class ConfigValidator {
137174
this.validateRuleSchema(rule, Array.isArray(options) ? options.slice(1) : []);
138175
}
139176
} catch (err) {
140-
const enhancedMessage = `Configuration for rule "${ruleId}" is invalid:\n${err.message}`;
177+
let enhancedMessage = err.code === "ESLINT_INVALID_RULE_OPTIONS_SCHEMA"
178+
? `Error while processing options validation schema of rule '${ruleId}': ${err.message}`
179+
: `Configuration for rule "${ruleId}" is invalid:\n${err.message}`;
141180

142181
if (typeof source === "string") {
143-
throw new Error(`${source}:\n\t${enhancedMessage}`);
144-
} else {
145-
throw new Error(enhancedMessage);
182+
enhancedMessage = `${source}:\n\t${enhancedMessage}`;
146183
}
184+
185+
const enhancedError = new Error(enhancedMessage, { cause: err });
186+
187+
if (err.code) {
188+
enhancedError.code = err.code;
189+
}
190+
191+
throw enhancedError;
147192
}
148193
}
149194

tests/fixtures/rules/custom-rule.cjs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
module.exports = function(context) {
1+
"use strict";
22

3-
"use strict";
3+
module.exports = {
4+
meta: {
5+
schema: []
6+
},
47

5-
return {
6-
"Identifier": function(node) {
7-
if (node.name === "foo") {
8-
context.report(node, "Identifier cannot be named 'foo'.");
8+
create(context) {
9+
return {
10+
"Identifier": function(node) {
11+
if (node.name === "foo") {
12+
context.report(node, "Identifier cannot be named 'foo'.");
13+
}
914
}
10-
}
11-
};
12-
15+
};
16+
}
1317
};
14-
15-
module.exports.schema = [];
Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"use strict";
22

3-
module.exports = function(context) {
3+
module.exports = {
4+
create(context) {
5+
return {
46

5-
return {
7+
"Literal": function(node) {
8+
if (typeof node.value === 'string') {
9+
context.report(node, "String!");
10+
}
611

7-
"Literal": function(node) {
8-
if (typeof node.value === 'string') {
9-
context.report(node, "String!");
1012
}
11-
12-
}
13-
};
13+
};
14+
}
1415
};
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"use strict";
22

3-
module.exports = function(context) {
3+
module.exports = {
4+
create (context) {
5+
return {
46

5-
return {
6-
7-
"Literal": function(node) {
8-
context.report(node, "Literal!");
9-
}
10-
};
7+
"Literal": function(node) {
8+
context.report(node, "Literal!");
9+
}
10+
};
11+
}
1112
};

tests/fixtures/rules/make-syntax-error-rule.cjs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
module.exports = function(context) {
2-
return {
3-
Program: function(node) {
4-
context.report({
5-
node: node,
6-
message: "ERROR",
7-
fix: function(fixer) {
8-
return fixer.insertTextAfter(node, "this is a syntax error.");
9-
}
10-
});
11-
}
12-
};
1+
module.exports = {
2+
meta: {
3+
schema: []
4+
},
5+
create(context) {
6+
return {
7+
Program: function(node) {
8+
context.report({
9+
node: node,
10+
message: "ERROR",
11+
fix: function(fixer) {
12+
return fixer.insertTextAfter(node, "this is a syntax error.");
13+
}
14+
});
15+
}
16+
};
17+
}
1318
};
14-
module.exports.schema = [];
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
module.exports = function() {
1+
"use strict";
22

3-
"use strict";
4-
return (null).something;
3+
module.exports = {
4+
create() {
5+
return (null).something;
6+
}
57
};

0 commit comments

Comments
 (0)