diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 398f210135b..fe0e468916b 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -305,6 +305,36 @@ 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 //------------------------------------------------------------------------------ @@ -521,6 +551,9 @@ class RuleTester { ].concat(scenarioErrors).join("\n")); } + if (typeof rule === "function") { + emitLegacyRuleAPIWarning(ruleName); + } linter.defineRule(ruleName, Object.assign({}, rule, { @@ -578,6 +611,15 @@ class RuleTester { if (hasOwnProperty(item, "options")) { 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); + } config.rules[ruleName] = [1].concat(item.options); } else { config.rules[ruleName] = 1; diff --git a/tests/fixtures/testers/rule-tester/modify-ast-at-first.js b/tests/fixtures/testers/rule-tester/modify-ast-at-first.js index 831d4a06ca7..a7a80f1f16f 100644 --- a/tests/fixtures/testers/rule-tester/modify-ast-at-first.js +++ b/tests/fixtures/testers/rule-tester/modify-ast-at-first.js @@ -9,30 +9,36 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - return { - "Program": function(node) { - node.body.push({ - "type": "Identifier", - "name": "modified", - "range": [0, 8], - "loc": { - "start": { - "line": 1, - "column": 0 - }, - "end": { - "line": 1, - "column": 8 +module.exports = { + meta: { + type: "problem", + schema: [] + }, + create(context) { + return { + "Program": function(node) { + node.body.push({ + "type": "Identifier", + "name": "modified", + "range": [0, 8], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 8 + } } - } - }); - }, + }); + }, - "Identifier": function(node) { - if (node.name === "bar") { - context.report({message: "error", node: node}); + "Identifier": function(node) { + if (node.name === "bar") { + context.report({message: "error", node: node}); + } } - } - }; + }; + }, }; diff --git a/tests/fixtures/testers/rule-tester/modify-ast-at-last.js b/tests/fixtures/testers/rule-tester/modify-ast-at-last.js index f093b191c3c..7c7c0c87939 100644 --- a/tests/fixtures/testers/rule-tester/modify-ast-at-last.js +++ b/tests/fixtures/testers/rule-tester/modify-ast-at-last.js @@ -9,30 +9,36 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - return { - "Program:exit": function(node) { - node.body.push({ - "type": "Identifier", - "name": "modified", - "range": [0, 8], - "loc": { - "start": { - "line": 1, - "column": 0 - }, - "end": { - "line": 1, - "column": 8 +module.exports = { + meta: { + type: "problem", + schema: [] + }, + create(context) { + return { + "Program:exit": function(node) { + node.body.push({ + "type": "Identifier", + "name": "modified", + "range": [0, 8], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 8 + } } - } - }); - }, + }); + }, - "Identifier": function(node) { - if (node.name === "bar") { - context.report({message: "error", node: node}); + "Identifier": function(node) { + if (node.name === "bar") { + context.report({message: "error", node: node}); + } } - } - }; + }; + }, }; diff --git a/tests/fixtures/testers/rule-tester/modify-ast.js b/tests/fixtures/testers/rule-tester/modify-ast.js index 82a7c48ffc5..45f46a662e4 100644 --- a/tests/fixtures/testers/rule-tester/modify-ast.js +++ b/tests/fixtures/testers/rule-tester/modify-ast.js @@ -9,14 +9,20 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - return { - "Identifier": function(node) { - node.name += "!"; +module.exports = { + meta: { + type: "problem", + schema: [] + }, + create(context) { + return { + "Identifier": function(node) { + node.name += "!"; - if (node.name === "bar!") { - context.report({message: "error", node: node}); + if (node.name === "bar!") { + context.report({message: "error", node: node}); + } } - } - }; + }; + }, }; diff --git a/tests/fixtures/testers/rule-tester/no-eval.js b/tests/fixtures/testers/rule-tester/no-eval.js index 0d57cb6cd7b..dc6e869888e 100644 --- a/tests/fixtures/testers/rule-tester/no-eval.js +++ b/tests/fixtures/testers/rule-tester/no-eval.js @@ -3,20 +3,24 @@ * @author Nicholas C. Zakas */ +"use strict"; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - - "use strict"; - - return { - "CallExpression": function(node) { - if (node.callee.name === "eval") { - context.report(node, "eval sucks."); - } - } - }; - +module.exports = { + meta: { + type: "problem", + schema: [], + }, + create(context) { + return { + CallExpression: function (node) { + if (node.callee.name === "eval") { + context.report(node, "eval sucks."); + } + }, + }; + }, }; diff --git a/tests/fixtures/testers/rule-tester/no-invalid-args.js b/tests/fixtures/testers/rule-tester/no-invalid-args.js index 4c2e7015cbb..d1eb2ad7199 100644 --- a/tests/fixtures/testers/rule-tester/no-invalid-args.js +++ b/tests/fixtures/testers/rule-tester/no-invalid-args.js @@ -3,20 +3,28 @@ * @author Mathias Schreck */ +"use strict"; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - "use strict"; - - var config = context.options[0]; +module.exports = { + meta: { + type: "problem", + schema: [{ + type: "boolean" + }] + }, + create(context) { + var config = context.options[0]; - return { - "Program": function(node) { - if (config === true) { - context.report(node, "Invalid args"); + return { + "Program": function(node) { + if (config === true) { + context.report(node, "Invalid args"); + } } - } - }; + }; + } }; diff --git a/tests/fixtures/testers/rule-tester/no-invalid-schema.js b/tests/fixtures/testers/rule-tester/no-invalid-schema.js index fdf290dc62e..affe38053ab 100644 --- a/tests/fixtures/testers/rule-tester/no-invalid-schema.js +++ b/tests/fixtures/testers/rule-tester/no-invalid-schema.js @@ -3,26 +3,26 @@ * @author Brandon Mills */ +"use strict"; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - "use strict"; - - var config = context.options[0]; - - return { - "Program": function(node) { - if (config) { - context.report(node, "Expected nothing."); +module.exports = { + meta: { + type: "problem", + schema: [{ + "enum": [] + }] + }, + create(context) { + return { + "Program": function(node) { + if (config) { + context.report(node, "Expected nothing."); + } } - } - }; + }; + }, }; - -module.exports.schema = [ - { - "enum": [] - } -]; diff --git a/tests/fixtures/testers/rule-tester/no-schema-violation.js b/tests/fixtures/testers/rule-tester/no-schema-violation.js index 2a9f65e2161..7876f25305b 100644 --- a/tests/fixtures/testers/rule-tester/no-schema-violation.js +++ b/tests/fixtures/testers/rule-tester/no-schema-violation.js @@ -3,26 +3,27 @@ * @author Brandon Mills */ +"use strict"; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - "use strict"; - - var config = context.options[0]; - - return { - "Program": function(node) { - if (config && config !== "foo") { - context.report(node, "Expected foo."); +module.exports = { + meta: { + type: "problem", + schema: [{ + "enum": ["foo"] + }] + }, + create(context) { + const config = context.options[0]; + return { + "Program": function(node) { + if (config && config !== "foo") { + context.report(node, "Expected foo."); + } } - } - }; + }; + }, }; - -module.exports.schema = [ - { - "enum": ["foo"] - } -]; diff --git a/tests/fixtures/testers/rule-tester/no-test-filename b/tests/fixtures/testers/rule-tester/no-test-filename index 752c41f0dbf..b3cde257352 100644 --- a/tests/fixtures/testers/rule-tester/no-test-filename +++ b/tests/fixtures/testers/rule-tester/no-test-filename @@ -3,18 +3,24 @@ * @author Stefan Lau */ +"use strict"; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - "use strict"; - - return { - "Program": function(node) { - if (context.getFilename() === '') { - context.report(node, "Filename test was not defined."); +module.exports = { + meta: { + type: "problem", + schema: [] + }, + create(context) { + return { + "Program": function(node) { + if (context.getFilename() === '') { + context.report(node, "Filename test was not defined."); + } } - } - }; + }; + } }; diff --git a/tests/fixtures/testers/rule-tester/no-test-global.js b/tests/fixtures/testers/rule-tester/no-test-global.js index b5fa4c3bf92..6703cc62100 100644 --- a/tests/fixtures/testers/rule-tester/no-test-global.js +++ b/tests/fixtures/testers/rule-tester/no-test-global.js @@ -7,21 +7,27 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - "use strict"; +"use strict"; - return { - "Program": function(node) { - var globals = context.getScope().variables.map(function (variable) { - return variable.name; - }); +module.exports = { + meta: { + type: "problem", + schema: [], + }, + create(context) { + return { + "Program": function(node) { + var globals = context.getScope().variables.map(function (variable) { + return variable.name; + }); - if (globals.indexOf("test") === -1) { - context.report(node, "Global variable test was not defined."); + if (globals.indexOf("test") === -1) { + context.report(node, "Global variable test was not defined."); + } + if (globals.indexOf("foo") !== -1) { + context.report(node, "Global variable foo should not be used."); + } } - if (globals.indexOf("foo") !== -1) { - context.report(node, "Global variable foo should not be used."); - } - } - }; + }; + }, }; diff --git a/tests/fixtures/testers/rule-tester/no-test-settings.js b/tests/fixtures/testers/rule-tester/no-test-settings.js index 07ecfa7bca6..a67ebc23ff5 100644 --- a/tests/fixtures/testers/rule-tester/no-test-settings.js +++ b/tests/fixtures/testers/rule-tester/no-test-settings.js @@ -3,18 +3,27 @@ * @author Ilya Volodin */ +"use strict"; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - "use strict"; - - return { - "Program": function(node) { - if (!context.settings || !context.settings.test) { - context.report(node, "Global settings test was not defined."); - } - } - }; +module.exports = { + meta: { + type: "problem", + schema: [], + }, + create(context) { + return { + Program: function (node) { + if (!context.settings || !context.settings.test) { + context.report( + node, + "Global settings test was not defined." + ); + } + }, + }; + }, }; diff --git a/tests/fixtures/testers/rule-tester/no-var.js b/tests/fixtures/testers/rule-tester/no-var.js index 5841f15bfa1..96a410fe78a 100644 --- a/tests/fixtures/testers/rule-tester/no-var.js +++ b/tests/fixtures/testers/rule-tester/no-var.js @@ -10,13 +10,11 @@ "use strict"; module.exports = { - meta: { - fixable: "code" + fixable: "code", + schema: [] }, - create(context) { - var sourceCode = context.getSourceCode(); return { diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index ba12bdc38a2..0e35258750c 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -1710,73 +1710,6 @@ describe("RuleTester", () => { }, "Error must specify 'messageId' if 'data' is used."); }); - // fixable rules with or without `meta` property - it("should not throw an error if a rule that has `meta.fixable` produces fixes", () => { - const replaceProgramWith5Rule = { - meta: { - fixable: "code" - }, - create(context) { - return { - Program(node) { - context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); - } - }; - } - }; - - ruleTester.run("replaceProgramWith5", replaceProgramWith5Rule, { - valid: [], - invalid: [ - { code: "var foo = bar;", output: "5", errors: 1 } - ] - }); - }); - it("should throw an error if a new-format rule that doesn't have `meta` produces fixes", () => { - const replaceProgramWith5Rule = { - create(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); - }); - 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)", () => { ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { @@ -2295,6 +2228,265 @@ describe("RuleTester", () => { }); }); + describe("deprecations", () => { + let processStub; + const ruleWithNoSchema = { + meta: { + type: "suggestion" + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + const ruleWithNoMeta = { + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + beforeEach(() => { + processStub = sinon.stub(process, "emitWarning"); + }); + + afterEach(() => { + processStub.restore(); + }); + + it("should log a deprecation warning when using the legacy function-style API for rule", () => { + + /** + * Legacy-format rule (a function instead of an object with `create` method). + * @param {RuleContext} context The ESLint rule context object. + * @returns {Object} Listeners. + */ + function functionStyleRule(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + + 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" + ] + ); + }); + + it("should log a deprecation warning when meta is not defined for the rule", () => { + 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" + ] + ); + }); + + it("should log a deprecation warning when schema is not defined for the rule", () => { + 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" + ] + ); + }); + + it("should log a deprecation warning when schema is `undefined`", () => { + const ruleWithUndefinedSchema = { + meta: { + type: "problem", + // eslint-disable-next-line no-undefined -- intentioally added for test case + schema: undefined + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + 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" + ] + ); + }); + + it("should log a deprecation warning when schema is `null`", () => { + const ruleWithNullSchema = { + meta: { + type: "problem", + schema: null + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + ruleTester.run("rule-with-null-schema", ruleWithNullSchema, { + 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-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" + ] + ); + }); + + it("should not log a deprecation warning when schema is an empty array", () => { + const ruleWithEmptySchema = { + meta: { + type: "suggestion", + schema: [] + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + ruleTester.run("rule-with-no-options", ruleWithEmptySchema, { + 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", () => { + 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", () => { + const ruleWithSchema = { + meta: { + type: "suggestion", + schema: [{ + type: "boolean" + }] + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + ruleTester.run("rule-with-schema", ruleWithSchema, { + valid: [], + invalid: [ + { 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", () => { + 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", () => { + 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", () => { + 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()`"); + }); + }); + /** * Asserts that a particular value will be emitted from an EventEmitter. * @param {EventEmitter} emitter The emitter that should emit a value