diff --git a/docs/src/developer-guide/working-with-rules-deprecated.md b/docs/src/developer-guide/working-with-rules-deprecated.md index b2fb8522ff4..6d476cdbb98 100644 --- a/docs/src/developer-guide/working-with-rules-deprecated.md +++ b/docs/src/developer-guide/working-with-rules-deprecated.md @@ -3,7 +3,7 @@ title: Working with Rules (Deprecated) --- -**Note:** This page covers the deprecated rule format for ESLint <= 2.13.1. [This is the most recent rule format](./working-with-rules). +**Note:** This page covers the deprecated function-style rule format for ESLint <= 2.13.1. [This is the most recent rule format](./working-with-rules). This format has been removed as of ESLint 9.0.0. Each rule in ESLint has two files named with its identifier (for example, `no-extra-semi`). diff --git a/docs/src/developer-guide/working-with-rules.md b/docs/src/developer-guide/working-with-rules.md index 0e92ccfe748..018a05c4bf8 100644 --- a/docs/src/developer-guide/working-with-rules.md +++ b/docs/src/developer-guide/working-with-rules.md @@ -80,7 +80,7 @@ The source file for a rule exports an object with the following properties. **Important:** the `hasSuggestions` property is mandatory for rules that provide suggestions. If this property isn't set to `true`, ESLint will throw an error whenever the rule attempts to produce a suggestion. Omit the `hasSuggestions` property if the rule does not provide suggestions. -* `schema` (array) specifies the [options](#options-schemas) so ESLint can prevent invalid [rule configurations](../user-guide/configuring/rules#configuring-rules) +* `schema` (array) specifies the [options](#options-schemas) so ESLint can prevent invalid [rule configurations](../user-guide/configuring/rules#configuring-rules). Mandatory when a rule has options. * `deprecated` (boolean) indicates whether the rule has been deprecated. You may omit the `deprecated` property if the rule has not been deprecated. @@ -533,6 +533,7 @@ The `quotes` rule in this example has one option, `"double"` (the `error` is the ```js module.exports = { + meta: { schema: [/* ... */] }, create: function(context) { var isDouble = (context.options[0] === "double"); @@ -626,7 +627,9 @@ Please note that the following methods have been deprecated and will be removed ### Options Schemas -Rules may export a `schema` property, which is a [JSON schema](https://json-schema.org/) format description of a rule's options which will be used by ESLint to validate configuration options and prevent invalid or unexpected inputs before they are passed to the rule in `context.options`. +Rules may export a `meta.schema` property, which is a [JSON schema](https://json-schema.org/) format description of a rule's options which will be used by ESLint to validate configuration options and prevent invalid or unexpected inputs before they are passed to the rule in `context.options`. + +Providing a schema is mandatory when a rule has options. However, it is possible to opt-out of providing a schema using `schema: false`, but doing so is discouraged as it increases the chance of bugs and mistakes. Rules without options can simply omit the schema property or use `schema: []`, both of which prevent any options from being passed. There are two formats for a rule's exported `schema`. The first is a full JSON Schema object describing all possible options the rule accepts, including the rule's error level as the first argument and any optional arguments thereafter. diff --git a/lib/config/flat-config-helpers.js b/lib/config/flat-config-helpers.js index e00c56434cd..a1b53646b4e 100644 --- a/lib/config/flat-config-helpers.js +++ b/lib/config/flat-config-helpers.js @@ -52,23 +52,24 @@ function getRuleFromConfig(ruleId, config) { const { pluginName, ruleName } = parseRuleId(ruleId); const plugin = config.plugins && config.plugins[pluginName]; - let rule = plugin && plugin.rules && plugin.rules[ruleName]; - - - // normalize function rules into objects - if (rule && typeof rule === "function") { - rule = { - create: rule - }; - } + const rule = plugin && plugin.rules && plugin.rules[ruleName]; return rule; } +const SCHEMA_NO_OPTIONS = { + type: "array", + minItems: 0, + maxItems: 0 +}; + +Object.freeze(SCHEMA_NO_OPTIONS); + /** * Gets a complete options schema for a rule. * @param {{create: Function, schema: (Array|null)}} rule A new-style rule object * @returns {Object} JSON Schema for the rule's options. + * @throws {Error} An error if the schema is a common no-op. */ function getRuleOptionsSchema(rule) { @@ -76,7 +77,17 @@ function getRuleOptionsSchema(rule) { return null; } - const schema = rule.schema || rule.meta && rule.meta.schema; + const schema = rule.meta && rule.meta.schema; + + // Check if the rule opted-out of specifying a schema. + if (schema === false) { + return null; + } + + // Check for no-op schema. + if (typeof schema === "object" && !Array.isArray(schema) && Object.keys(schema).length === 0) { + throw new Error("`schema: {}` is a no-op. For rules with options, please fill in a complete schema. For rules without options, please omit `schema` or use `schema: []`."); + } if (Array.isArray(schema)) { if (schema.length) { @@ -87,16 +98,12 @@ function getRuleOptionsSchema(rule) { maxItems: schema.length }; } - return { - type: "array", - minItems: 0, - maxItems: 0 - }; + return SCHEMA_NO_OPTIONS; } // Given a full schema, leave it alone - return schema || null; + return schema || SCHEMA_NO_OPTIONS; } diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 764ea5f5795..6590b90c3c6 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -1946,7 +1946,7 @@ class Linter { /** * Defines a new linting rule. * @param {string} ruleId A unique rule identifier - * @param {Function | Rule} ruleModule Function from context to object mapping AST node types to event handlers + * @param {Rule} ruleModule The rule module * @returns {void} */ defineRule(ruleId, ruleModule) { @@ -1956,7 +1956,7 @@ class Linter { /** * Defines many new linting rules. - * @param {Record} rulesToDefine map from unique rule identifier to rule + * @param {Record} rulesToDefine map from unique rule identifier to rule * @returns {void} */ defineRules(rulesToDefine) { diff --git a/lib/linter/rules.js b/lib/linter/rules.js index 647bab68784..d925590ed42 100644 --- a/lib/linter/rules.js +++ b/lib/linter/rules.js @@ -12,20 +12,6 @@ const builtInRules = require("../rules"); -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Normalizes a rule module to the new-style API - * @param {(Function|{create: Function})} rule A rule object, which can either be a function - * ("old-style") or an object with a `create` method ("new-style") - * @returns {{create: Function}} A new-style rule. - */ -function normalizeRule(rule) { - return typeof rule === "function" ? Object.assign({ create: rule }, rule) : rule; -} - //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -41,11 +27,15 @@ class Rules { /** * Registers a rule module for rule id in storage. * @param {string} ruleId Rule id (file name). - * @param {Function} ruleModule Rule handler. + * @param {RuleModule} ruleModule Rule handler. + * @throws {TypeError} If the rule is using the deprecated function-style instead of object-style. * @returns {void} */ define(ruleId, ruleModule) { - this._rules[ruleId] = normalizeRule(ruleModule); + if (typeof ruleModule === "function") { + throw new TypeError(`"${ruleId}" rule is using the deprecated function-style format. Please use object-style format: https://eslint.org/docs/developer-guide/working-with-rules`); + } + this._rules[ruleId] = ruleModule; } /** diff --git a/lib/rule-tester/flat-rule-tester.js b/lib/rule-tester/flat-rule-tester.js index b45b5d3c3db..838a7b2516c 100644 --- a/lib/rule-tester/flat-rule-tester.js +++ b/lib/rule-tester/flat-rule-tester.js @@ -447,7 +447,7 @@ class FlatRuleTester { /** * Adds a new rule test to execute. * @param {string} ruleName The name of the rule to run. - * @param {Function} rule The rule to test. + * @param {RuleModule} rule The rule to test. * @param {{ * valid: (ValidTestCase | string)[], * invalid: InvalidTestCase[] @@ -516,7 +516,7 @@ class FlatRuleTester { // freezeDeeply(context.languageOptions); - return (typeof rule === "function" ? rule : rule.create)(context); + return rule.create(context); } }) } diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 62fd467e0b0..1a325352286 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -305,36 +305,6 @@ function getCommentsDeprecation() { ); } -/** - * Emit a deprecation warning if function-style format is being used. - * @param {string} ruleName Name of the rule. - * @returns {void} - */ -function emitLegacyRuleAPIWarning(ruleName) { - if (!emitLegacyRuleAPIWarning[`warned-${ruleName}`]) { - emitLegacyRuleAPIWarning[`warned-${ruleName}`] = true; - process.emitWarning( - `"${ruleName}" rule is using the deprecated function-style format and will stop working in ESLint v9. Please use object-style format: https://eslint.org/docs/developer-guide/working-with-rules`, - "DeprecationWarning" - ); - } -} - -/** - * Emit a deprecation warning if rule has options but is missing the "meta.schema" property - * @param {string} ruleName Name of the rule. - * @returns {void} - */ -function emitMissingSchemaWarning(ruleName) { - if (!emitMissingSchemaWarning[`warned-${ruleName}`]) { - emitMissingSchemaWarning[`warned-${ruleName}`] = true; - process.emitWarning( - `"${ruleName}" rule has options but is missing the "meta.schema" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/developer-guide/working-with-rules#options-schemas`, - "DeprecationWarning" - ); - } -} - //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -509,7 +479,7 @@ class RuleTester { /** * Define a rule for one particular run of tests. * @param {string} name The name of the rule to define. - * @param {Function} rule The rule definition. + * @param {RuleModule} rule The rule definition. * @returns {void} */ defineRule(name, rule) { @@ -519,7 +489,7 @@ class RuleTester { /** * Adds a new rule test to execute. * @param {string} ruleName The name of the rule to run. - * @param {Function} rule The rule to test. + * @param {RuleModule} rule The rule to test. * @param {{ * valid: (ValidTestCase | string)[], * invalid: InvalidTestCase[] @@ -552,7 +522,9 @@ class RuleTester { } if (typeof rule === "function") { - emitLegacyRuleAPIWarning(ruleName); + throw new Error( + `"${ruleName}" rule is using the deprecated function-style format. Please use object-style format: https://eslint.org/docs/developer-guide/working-with-rules` + ); } linter.defineRule(ruleName, Object.assign({}, rule, { @@ -563,7 +535,7 @@ class RuleTester { freezeDeeply(context.settings); freezeDeeply(context.parserOptions); - return (typeof rule === "function" ? rule : rule.create)(context); + return rule.create(context); } })); @@ -613,12 +585,13 @@ class RuleTester { assert(Array.isArray(item.options), "options must be an array"); if ( item.options.length > 0 && - typeof rule === "object" && ( !rule.meta || (rule.meta && (typeof rule.meta.schema === "undefined" || rule.meta.schema === null)) ) ) { - emitMissingSchemaWarning(ruleName); + throw new Error( + `"${ruleName}" rule has options but is missing the "meta.schema" property. Please add a schema: https://eslint.org/docs/developer-guide/working-with-rules#options-schemas` + ); } config.rules[ruleName] = [1].concat(item.options); } else { diff --git a/lib/rules/no-constructor-return.js b/lib/rules/no-constructor-return.js index 911a32abcae..fecba5394d7 100644 --- a/lib/rules/no-constructor-return.js +++ b/lib/rules/no-constructor-return.js @@ -20,7 +20,7 @@ module.exports = { url: "https://eslint.org/docs/rules/no-constructor-return" }, - schema: {}, + schema: [], fixable: null, diff --git a/lib/shared/config-validator.js b/lib/shared/config-validator.js index 47353ac4814..9ab26698487 100644 --- a/lib/shared/config-validator.js +++ b/lib/shared/config-validator.js @@ -47,17 +47,36 @@ const severityMap = { off: 0 }; +const SCHEMA_NO_OPTIONS = { + type: "array", + minItems: 0, + maxItems: 0 +}; + +Object.freeze(SCHEMA_NO_OPTIONS); + /** * Gets a complete options schema for a rule. * @param {{create: Function, schema: (Array|null)}} rule A new-style rule object * @returns {Object} JSON Schema for the rule's options. + * @throws {Error} An error if the schema is a common no-op. */ function getRuleOptionsSchema(rule) { if (!rule) { return null; } - const schema = rule.schema || rule.meta && rule.meta.schema; + const schema = rule.meta && rule.meta.schema; + + // Check if the rule opted-out of specifying a schema. + if (schema === false) { + return null; + } + + // Check for no-op schema. + if (typeof schema === "object" && !Array.isArray(schema) && Object.keys(schema).length === 0) { + throw new Error("`schema: {}` is a no-op. For rules with options, please fill in a complete schema. For rules without options, please omit `schema` or use `schema: []`."); + } // Given a tuple of schemas, insert warning level at the beginning if (Array.isArray(schema)) { @@ -69,16 +88,12 @@ function getRuleOptionsSchema(rule) { maxItems: schema.length }; } - return { - type: "array", - minItems: 0, - maxItems: 0 - }; + return SCHEMA_NO_OPTIONS; } // Given a full schema, leave it alone - return schema || null; + return schema || SCHEMA_NO_OPTIONS; } /** diff --git a/lib/shared/types.js b/lib/shared/types.js index 20335f68a73..1b4d4d866a1 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -164,7 +164,7 @@ module.exports = {}; * @property {Record} [configs] The definition of plugin configs. * @property {Record} [environments] The definition of plugin environments. * @property {Record} [processors] The definition of plugin processors. - * @property {Record} [rules] The definition of plugin rules. + * @property {Record} [rules] The definition of plugin rules. */ /** diff --git a/tests/lib/linter/rules.js b/tests/lib/linter/rules.js index 31d6764dc26..9ef007e72bb 100644 --- a/tests/lib/linter/rules.js +++ b/tests/lib/linter/rules.js @@ -32,16 +32,14 @@ describe("rules", () => { assert.ok(rules.get(ruleId)); }); - it("should return the rule as an object with a create() method if the rule was defined as a function", () => { + it("throws when using deprecated function-style rule format", () => { /** * A rule that does nothing * @returns {void} */ function rule() {} - rule.schema = []; - rules.define("foo", rule); - assert.deepStrictEqual(rules.get("foo"), { create: rule, schema: [] }); + assert.throws(() => rules.define("foo", rule), "\"foo\" rule is using the deprecated function-style format. Please use object-style format: https://eslint.org/docs/developer-guide/working-with-rules"); }); it("should return the rule as-is if it was defined as an object with a create() method", () => { diff --git a/tests/lib/rule-tester/flat-rule-tester.js b/tests/lib/rule-tester/flat-rule-tester.js index bdc196e1653..643613a97b7 100644 --- a/tests/lib/rule-tester/flat-rule-tester.js +++ b/tests/lib/rule-tester/flat-rule-tester.js @@ -1701,30 +1701,6 @@ describe("FlatRuleTester", () => { }); }, /Fixable rules must set the `meta\.fixable` property/u); }); - it("should throw an error if a legacy-format rule produces fixes", () => { - - /** - * Legacy-format rule (a function instead of an object with `create` method). - * @param {RuleContext} context The ESLint rule context object. - * @returns {Object} Listeners. - */ - function replaceProgramWith5Rule(context) { - return { - Program(node) { - context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); - } - }; - } - - assert.throws(() => { - ruleTester.run("replaceProgramWith5", replaceProgramWith5Rule, { - valid: [], - invalid: [ - { code: "var foo = bar;", output: "5", errors: 1 } - ] - }); - }, /Fixable rules must set the `meta\.fixable` property/u); - }); describe("suggestions", () => { it("should pass with valid suggestions (tested using desc)", () => { diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index bba5f14bfbc..60a016d4e67 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2229,7 +2229,6 @@ describe("RuleTester", () => { }); describe("deprecations", () => { - let processStub; const ruleWithNoSchema = { meta: { type: "suggestion" @@ -2252,15 +2251,7 @@ describe("RuleTester", () => { } }; - beforeEach(() => { - processStub = sinon.stub(process, "emitWarning"); - }); - - afterEach(() => { - processStub.restore(); - }); - - it("should log a deprecation warning when using the legacy function-style API for rule", () => { + it("throws an error when using the legacy function-style API for rule", () => { /** * Legacy-format rule (a function instead of an object with `create` method). @@ -2275,60 +2266,36 @@ describe("RuleTester", () => { }; } - ruleTester.run("function-style-rule", functionStyleRule, { + assert.throws(() => ruleTester.run("function-style-rule", functionStyleRule, { valid: [], invalid: [ { code: "var foo = bar;", errors: 1 } ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - "\"function-style-rule\" rule is using the deprecated function-style format and will stop working in ESLint v9. Please use object-style format: https://eslint.org/docs/developer-guide/working-with-rules", - "DeprecationWarning" - ] - ); + }), + "\"function-style-rule\" rule is using the deprecated function-style format. Please use object-style format: https://eslint.org/docs/developer-guide/working-with-rules"); }); - it("should log a deprecation warning when meta is not defined for the rule", () => { - ruleTester.run("rule-with-no-meta-1", ruleWithNoMeta, { + it("throws an error when meta is not defined for the rule", () => { + assert.throws(() => ruleTester.run("rule-with-no-meta-1", ruleWithNoMeta, { valid: [], invalid: [ { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - "\"rule-with-no-meta-1\" rule has options but is missing the \"meta.schema\" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/developer-guide/working-with-rules#options-schemas", - "DeprecationWarning" - ] - ); + }), + "\"rule-with-no-meta-1\" rule has options but is missing the \"meta.schema\" property. Please add a schema: https://eslint.org/docs/developer-guide/working-with-rules#options-schemas"); }); - it("should log a deprecation warning when schema is not defined for the rule", () => { - ruleTester.run("rule-with-no-schema-1", ruleWithNoSchema, { + it("throws an error when schema is not defined for the rule", () => { + assert.throws(() => ruleTester.run("rule-with-no-schema-1", ruleWithNoSchema, { valid: [], invalid: [ { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - "\"rule-with-no-schema-1\" rule has options but is missing the \"meta.schema\" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/developer-guide/working-with-rules#options-schemas", - "DeprecationWarning" - ] - ); + }), + "\"rule-with-no-schema-1\" rule has options but is missing the \"meta.schema\" property. Please add a schema: https://eslint.org/docs/developer-guide/working-with-rules#options-schemas"); }); - it("should log a deprecation warning when schema is `undefined`", () => { + it("throws an error when schema is `undefined`", () => { const ruleWithUndefinedSchema = { meta: { type: "problem", @@ -2344,24 +2311,16 @@ describe("RuleTester", () => { } }; - ruleTester.run("rule-with-undefined-schema", ruleWithUndefinedSchema, { + assert.throws(() => ruleTester.run("rule-with-undefined-schema", ruleWithUndefinedSchema, { valid: [], invalid: [ { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - "\"rule-with-undefined-schema\" rule has options but is missing the \"meta.schema\" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/developer-guide/working-with-rules#options-schemas", - "DeprecationWarning" - ] - ); + }), + "\"rule-with-undefined-schema\" rule has options but is missing the \"meta.schema\" property. Please add a schema: https://eslint.org/docs/developer-guide/working-with-rules#options-schemas"); }); - it("should log a deprecation warning when schema is `null`", () => { + it("throws an error when schema is `null`", () => { const ruleWithNullSchema = { meta: { type: "problem", @@ -2376,24 +2335,37 @@ describe("RuleTester", () => { } }; - ruleTester.run("rule-with-null-schema", ruleWithNullSchema, { + assert.throws(() => ruleTester.run("rule-with-null-schema", ruleWithNullSchema, { valid: [], invalid: [ { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } ] - }); + }), + "\"rule-with-null-schema\" rule has options but is missing the \"meta.schema\" property. Please add a schema: https://eslint.org/docs/developer-guide/working-with-rules#options-schemas"); + }); + + it("throws an error when using no-op schema {}", () => { + const ruleWithEmptyObjectSchema = { + meta: { schema: {} }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - "\"rule-with-null-schema\" rule has options but is missing the \"meta.schema\" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/developer-guide/working-with-rules#options-schemas", - "DeprecationWarning" + assert.throws(() => ruleTester.run("rule-with-empty-object-schema", ruleWithEmptyObjectSchema, { + valid: [], + invalid: [ + { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } ] - ); + }), + "`schema: {}` is a no-op. For rules with options, please fill in a complete schema. For rules without options, please omit `schema` or use `schema: []`."); }); - it("should not log a deprecation warning when schema is an empty array", () => { + it("should not throw an error when schema is an empty array", () => { const ruleWithEmptySchema = { meta: { type: "suggestion", @@ -2412,22 +2384,18 @@ describe("RuleTester", () => { valid: [], invalid: [{ code: "var foo = bar;", errors: 1 }] }); - - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); }); - it("When the rule is an object-style rule, the legacy rule API warning is not emitted", () => { + it("should not throw an error when the rule is an object-style rule", () => { ruleTester.run("rule-with-no-schema-2", ruleWithNoSchema, { valid: [], invalid: [ { code: "var foo = bar;", errors: 1 } ] }); - - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); }); - it("When the rule has meta.schema and there are test cases with options, the missing schema warning is not emitted", () => { + it("should not throw an error when the rule has meta.schema and there are test cases with options", () => { const ruleWithSchema = { meta: { type: "suggestion", @@ -2450,40 +2418,52 @@ describe("RuleTester", () => { { code: "var foo = bar;", options: [true], errors: 1 } ] }); - - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); }); - it("When the rule does not have meta, but there are no test cases with options, the missing schema warning is not emitted", () => { + it("should not throw an error when the rule does not have meta, but there are no test cases with options", () => { ruleTester.run("rule-with-no-meta-2", ruleWithNoMeta, { valid: [], invalid: [ { code: "var foo = bar;", errors: 1 } ] }); - - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); }); - it("When the rule has meta without meta.schema, but there are no test cases with options, the missing schema warning is not emitted", () => { + it("should not throw an error when the rule has meta without meta.schema, but there are no test cases with options", () => { ruleTester.run("rule-with-no-schema-3", ruleWithNoSchema, { valid: [], invalid: [ { code: "var foo = bar;", errors: 1 } ] }); - - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); }); - it("When the rule has meta without meta.schema, and some test cases have options property but it's an empty array, the missing schema warning is not emitted", () => { + it("should not throw an error when the rule has meta without meta.schema, and some test cases have options property but it's an empty array", () => { ruleTester.run("rule-with-no-schema-4", ruleWithNoSchema, { valid: [], invalid: [ { code: "var foo = bar;", options: [], errors: 1 } ] }); + }); - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); + it("should not throw an error when rule has options and opts-out of providing as schema", () => { + const ruleWithSchemaOptOut = { + meta: { schema: false }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + ruleTester.run("rule-with-no-schema-4", ruleWithSchemaOptOut, { + valid: [], + invalid: [ + { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } + ] + }); }); }); diff --git a/tests/lib/shared/config-validator.js b/tests/lib/shared/config-validator.js index d54d26e360d..460b4b6a516 100644 --- a/tests/lib/shared/config-validator.js +++ b/tests/lib/shared/config-validator.js @@ -424,6 +424,32 @@ describe("Validator", () => { }); }); + it("should not modify object schema", () => { + linter.defineRule("mock-object-rule", mockObjectRule); + assert.deepStrictEqual(validator.getRuleOptionsSchema(ruleMapper("mock-object-rule")), { + enum: ["first", "second"] + }); + }); + + it("should allow no options when omitting schema", () => { + linter.defineRule("mock-schema-false-rule", { meta: {}, create() {} }); + assert.deepStrictEqual(validator.getRuleOptionsSchema(ruleMapper("mock-schema-false-rule")), { + type: "array", + minItems: 0, + maxItems: 0 + }); + }); + + it("should not have a schema upon opt-out", () => { + linter.defineRule("mock-schema-false-rule", { meta: { schema: false }, create() {} }); + assert.deepStrictEqual(validator.getRuleOptionsSchema(ruleMapper("mock-schema-false-rule")), null); + }); + + it("should disallow no-op schema {}", () => { + linter.defineRule("mock-empty-object-rule", { meta: { schema: {} }, create() {} }); + assert.throws(() => validator.getRuleOptionsSchema(ruleMapper("mock-empty-object-rule")), "`schema: {}` is a no-op. For rules with options, please fill in a complete schema. For rules without options, please omit `schema` or use `schema: []`."); + }); + }); describe("validateRuleOptions", () => { diff --git a/tests/tools/config-rule.js b/tests/tools/config-rule.js index c91f464c20b..e57c8146d64 100644 --- a/tests/tools/config-rule.js +++ b/tests/tools/config-rule.js @@ -303,11 +303,7 @@ describe("ConfigRule", () => { it("should allow to ignore deprecated rules", () => { const expectedRules = Array.from(builtInRules.entries()) - .filter(([, rule]) => { - const isDeprecated = (typeof rule === "function") ? rule.deprecated : rule.meta.deprecated; - - return !isDeprecated; - }) + .filter(([, rule]) => !rule.meta.deprecated) .map(([id]) => id), actualRules = Object.keys(ConfigRule.createCoreRuleConfigs(true)); diff --git a/tools/config-rule.js b/tools/config-rule.js index 91e7eaef5a4..fff484894b4 100644 --- a/tools/config-rule.js +++ b/tools/config-rule.js @@ -293,14 +293,11 @@ function generateConfigsFromSchema(schema) { */ function createCoreRuleConfigs(noDeprecated = false) { return Array.from(builtInRules).reduce((accumulator, [id, rule]) => { - const schema = (typeof rule === "function") ? rule.schema : rule.meta.schema; - const isDeprecated = (typeof rule === "function") ? rule.deprecated : rule.meta.deprecated; - - if (noDeprecated && isDeprecated) { + if (noDeprecated && rule.meta.deprecated) { return accumulator; } - accumulator[id] = generateConfigsFromSchema(schema); + accumulator[id] = generateConfigsFromSchema(rule.meta.schema); return accumulator; }, {}); }