From 85fcf12fec5fb4b0407a8e232d5ea58e01e44e79 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Mon, 6 May 2024 07:36:10 -0500 Subject: [PATCH 1/8] port ruletester --- packages/rule-tester/src/RuleTester.ts | 28 ++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 324700fa264a..b90676b57b34 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -215,10 +215,7 @@ export class RuleTester extends TestFramework { throw new Error(DUPLICATE_PARSER_ERROR_MESSAGE); } if (!test.filename) { - return { - ...test, - filename: getFilename(test.parserOptions), - }; + return { ...test, filename: getFilename(test.parserOptions) }; } return test; }; @@ -444,6 +441,8 @@ export class RuleTester extends TestFramework { output: string; beforeAST: TSESTree.Program; afterAST: TSESTree.Program; + config: RuleTesterConfig; + filename?: string; } { let config: TesterConfigWithDefaults = merge({}, this.#testerConfig); let code; @@ -617,6 +616,8 @@ export class RuleTester extends TestFramework { // is definitely assigned within the `rule-tester/validate-ast` rule // eslint-disable-next-line @typescript-eslint/no-non-null-assertion afterAST: cloneDeeplyExcludesParent(afterAST!), + config, + filename, }; } @@ -981,6 +982,25 @@ export class RuleTester extends TestFramework { [actualSuggestion], ).output; + // Verify if suggestion fix makes a syntax error or not. + const errorMessageInSuggestion = this.#linter + .verify( + codeWithAppliedSuggestion, + result.config, + result.filename, + ) + .find(m => m.fatal); + + assert( + !errorMessageInSuggestion, + [ + 'A fatal parsing error occurred in suggestion fix.', + `Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`, + 'Suggestion output:', + codeWithAppliedSuggestion, + ].join('\n'), + ); + assert.strictEqual( codeWithAppliedSuggestion, expectedSuggestion.output, From 3c86b7b8b5fa9184cf6c933f7cf372853ec55ee2 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Mon, 6 May 2024 07:40:02 -0500 Subject: [PATCH 2/8] port tests from eslint --- .../tests/eslint-base/eslint-base.test.js | 5945 +++++++++-------- 1 file changed, 3262 insertions(+), 2683 deletions(-) diff --git a/packages/rule-tester/tests/eslint-base/eslint-base.test.js b/packages/rule-tester/tests/eslint-base/eslint-base.test.js index 8304dee78f95..8dae8336e345 100644 --- a/packages/rule-tester/tests/eslint-base/eslint-base.test.js +++ b/packages/rule-tester/tests/eslint-base/eslint-base.test.js @@ -14,25 +14,25 @@ */ /* eslint-disable */ -"use strict"; +'use strict'; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ -const sinon = require("sinon"), - EventEmitter = require("events"), - { RuleTester } = require("../../dist/RuleTester"), - assert = require("chai").assert, - nodeAssert = require("assert"), - espree = require("espree"); +const sinon = require('sinon'), + EventEmitter = require('events'), + { RuleTester } = require('../../dist/RuleTester'), + assert = require('chai').assert, + nodeAssert = require('assert'), + espree = require('espree'); const NODE_ASSERT_STRICT_EQUAL_OPERATOR = (() => { - try { - nodeAssert.strictEqual(1, 2); - } catch (err) { - return err.operator; - } - throw new Error("unexpected successful assertion"); + try { + nodeAssert.strictEqual(1, 2); + } catch (err) { + return err.operator; + } + throw new Error('unexpected successful assertion'); })(); /** @@ -40,8 +40,7 @@ const NODE_ASSERT_STRICT_EQUAL_OPERATOR = (() => { * @returns {void} */ function noop() { - - // do nothing. + // do nothing. } //------------------------------------------------------------------------------ @@ -68,2845 +67,3425 @@ const ruleTesterTestEmitter = new EventEmitter(); // Tests //------------------------------------------------------------------------------ -describe("RuleTester", () => { +describe('RuleTester', () => { + // Stub `describe()` and `it()` while this test suite. + before(() => { + RuleTester.describe = function (text, method) { + ruleTesterTestEmitter.emit('describe', text, method); + return method.call(this); + }; + RuleTester.it = function (text, method) { + ruleTesterTestEmitter.emit('it', text, method); + return method.call(this); + }; + }); + after(() => { + RuleTester.describe = null; + RuleTester.it = null; + }); + + let ruleTester; + + /** + * A helper function to verify Node.js core error messages. + * @param {string} actual The actual input + * @param {string} expected The expected input + * @returns {Function} Error callback to verify that the message is correct + * for the actual and expected input. + */ + function assertErrorMatches(actual, expected) { + const err = new nodeAssert.AssertionError({ + actual, + expected, + operator: NODE_ASSERT_STRICT_EQUAL_OPERATOR, + }); + + return err.message; + } + + beforeEach(() => { + RuleTester.resetDefaultConfig(); + ruleTester = new RuleTester(); + }); + + describe('only', () => { + describe('`itOnly` accessor', () => { + describe('when `itOnly` is set', () => { + before(() => { + RuleTester.itOnly = sinon.spy(); + }); + after(() => { + RuleTester.itOnly = void 0; + }); + beforeEach(() => { + RuleTester.itOnly.resetHistory(); + ruleTester = new RuleTester(); + }); - // Stub `describe()` and `it()` while this test suite. - before(() => { - RuleTester.describe = function(text, method) { - ruleTesterTestEmitter.emit("describe", text, method); - return method.call(this); - }; - RuleTester.it = function(text, method) { - ruleTesterTestEmitter.emit("it", text, method); - return method.call(this); - }; - }); - after(() => { - RuleTester.describe = null; - RuleTester.it = null; - }); + it('is called by exclusive tests', () => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [ + { + code: 'const notVar = 42;', + only: true, + }, + ], + invalid: [], + }); - let ruleTester; + sinon.assert.calledWith(RuleTester.itOnly, 'const notVar = 42;'); + }); + }); - /** - * A helper function to verify Node.js core error messages. - * @param {string} actual The actual input - * @param {string} expected The expected input - * @returns {Function} Error callback to verify that the message is correct - * for the actual and expected input. - */ - function assertErrorMatches(actual, expected) { - const err = new nodeAssert.AssertionError({ - actual, - expected, - operator: NODE_ASSERT_STRICT_EQUAL_OPERATOR + describe('when `it` is set and has an `only()` method', () => { + before(() => { + RuleTester.it.only = () => {}; + sinon.spy(RuleTester.it, 'only'); + }); + after(() => { + RuleTester.it.only = void 0; + }); + beforeEach(() => { + RuleTester.it.only.resetHistory(); + ruleTester = new RuleTester(); }); - return err.message; - } + it('is called by tests with `only` set', () => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [ + { + code: 'const notVar = 42;', + only: true, + }, + ], + invalid: [], + }); - beforeEach(() => { - RuleTester.resetDefaultConfig(); - ruleTester = new RuleTester(); - }); + sinon.assert.calledWith(RuleTester.it.only, 'const notVar = 42;'); + }); + }); - describe("only", () => { - describe("`itOnly` accessor", () => { - describe("when `itOnly` is set", () => { - before(() => { - RuleTester.itOnly = sinon.spy(); - }); - after(() => { - RuleTester.itOnly = void 0; - }); - beforeEach(() => { - RuleTester.itOnly.resetHistory(); - ruleTester = new RuleTester(); - }); + describe('when global `it` is a function that has an `only()` method', () => { + let originalGlobalItOnly; - it("is called by exclusive tests", () => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [{ - code: "const notVar = 42;", - only: true - }], - invalid: [] - }); + before(() => { + /* + * We run tests with `--forbid-only`, so we have to override + * `it.only` to prevent the real one from being called. + */ + originalGlobalItOnly = it.only; + it.only = () => {}; + sinon.spy(it, 'only'); + }); + after(() => { + it.only = originalGlobalItOnly; + }); + beforeEach(() => { + it.only.resetHistory(); + ruleTester = new RuleTester(); + }); - sinon.assert.calledWith(RuleTester.itOnly, "const notVar = 42;"); - }); - }); + it('is called by tests with `only` set', () => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [ + { + code: 'const notVar = 42;', + only: true, + }, + ], + invalid: [], + }); - describe("when `it` is set and has an `only()` method", () => { - before(() => { - RuleTester.it.only = () => {}; - sinon.spy(RuleTester.it, "only"); - }); - after(() => { - RuleTester.it.only = void 0; - }); - beforeEach(() => { - RuleTester.it.only.resetHistory(); - ruleTester = new RuleTester(); - }); + sinon.assert.calledWith(it.only, 'const notVar = 42;'); + }); + }); - it("is called by tests with `only` set", () => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [{ - code: "const notVar = 42;", - only: true - }], - invalid: [] - }); + describe('when `describe` and `it` are overridden without `itOnly`', () => { + let originalGlobalItOnly; - sinon.assert.calledWith(RuleTester.it.only, "const notVar = 42;"); - }); + before(() => { + /* + * These tests override `describe` and `it` already, so we + * don't need to override them here. We do, however, need to + * remove `only` from the global `it` to prevent it from + * being used instead. + */ + originalGlobalItOnly = it.only; + it.only = void 0; + }); + after(() => { + it.only = originalGlobalItOnly; + }); + beforeEach(() => { + ruleTester = new RuleTester(); + }); + + it('throws an error recommending overriding `itOnly`', () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [ + { + code: 'const notVar = 42;', + only: true, + }, + ], + invalid: [], }); + }, 'Set `RuleTester.itOnly` to use `only` with a custom test framework.'); + }); + }); - describe("when global `it` is a function that has an `only()` method", () => { - let originalGlobalItOnly; + describe('when global `it` is a function that does not have an `only()` method', () => { + let originalGlobalIt; + let originalRuleTesterDescribe; + let originalRuleTesterIt; - before(() => { + before(() => { + originalGlobalIt = global.it; - /* - * We run tests with `--forbid-only`, so we have to override - * `it.only` to prevent the real one from being called. - */ - originalGlobalItOnly = it.only; - it.only = () => {}; - sinon.spy(it, "only"); - }); - after(() => { - it.only = originalGlobalItOnly; - }); - beforeEach(() => { - it.only.resetHistory(); - ruleTester = new RuleTester(); - }); + // eslint-disable-next-line no-global-assign -- Temporarily override Mocha global + it = () => {}; - it("is called by tests with `only` set", () => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [{ - code: "const notVar = 42;", - only: true - }], - invalid: [] - }); + /* + * These tests override `describe` and `it`, so we need to + * un-override them here so they won't interfere. + */ + originalRuleTesterDescribe = RuleTester.describe; + RuleTester.describe = void 0; + originalRuleTesterIt = RuleTester.it; + RuleTester.it = void 0; + }); + after(() => { + // eslint-disable-next-line no-global-assign -- Restore Mocha global + it = originalGlobalIt; + RuleTester.describe = originalRuleTesterDescribe; + RuleTester.it = originalRuleTesterIt; + }); + beforeEach(() => { + ruleTester = new RuleTester(); + }); - sinon.assert.calledWith(it.only, "const notVar = 42;"); - }); + it('throws an error explaining that the current test framework does not support `only`', () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [ + { + code: 'const notVar = 42;', + only: true, + }, + ], + invalid: [], }); + }, 'The current test framework does not support exclusive tests with `only`.'); + }); + }); + }); - describe("when `describe` and `it` are overridden without `itOnly`", () => { - let originalGlobalItOnly; - - before(() => { + describe('test cases', () => { + const ruleName = 'no-var'; + const rule = require('./fixtures/no-var'); - /* - * These tests override `describe` and `it` already, so we - * don't need to override them here. We do, however, need to - * remove `only` from the global `it` to prevent it from - * being used instead. - */ - originalGlobalItOnly = it.only; - it.only = void 0; - }); - after(() => { - it.only = originalGlobalItOnly; - }); - beforeEach(() => { - ruleTester = new RuleTester(); - }); + let originalRuleTesterIt; + let spyRuleTesterIt; + let originalRuleTesterItOnly; + let spyRuleTesterItOnly; - it("throws an error recommending overriding `itOnly`", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [{ - code: "const notVar = 42;", - only: true - }], - invalid: [] - }); - }, "Set `RuleTester.itOnly` to use `only` with a custom test framework."); - }); - }); + before(() => { + originalRuleTesterIt = RuleTester.it; + spyRuleTesterIt = sinon.spy(); + RuleTester.it = spyRuleTesterIt; + originalRuleTesterItOnly = RuleTester.itOnly; + spyRuleTesterItOnly = sinon.spy(); + RuleTester.itOnly = spyRuleTesterItOnly; + }); + after(() => { + RuleTester.it = originalRuleTesterIt; + RuleTester.itOnly = originalRuleTesterItOnly; + }); + beforeEach(() => { + spyRuleTesterIt.resetHistory(); + spyRuleTesterItOnly.resetHistory(); + ruleTester = new RuleTester(); + }); - describe("when global `it` is a function that does not have an `only()` method", () => { - let originalGlobalIt; - let originalRuleTesterDescribe; - let originalRuleTesterIt; - - before(() => { - originalGlobalIt = global.it; - - // eslint-disable-next-line no-global-assign -- Temporarily override Mocha global - it = () => {}; - - /* - * These tests override `describe` and `it`, so we need to - * un-override them here so they won't interfere. - */ - originalRuleTesterDescribe = RuleTester.describe; - RuleTester.describe = void 0; - originalRuleTesterIt = RuleTester.it; - RuleTester.it = void 0; - }); - after(() => { + it("isn't called for normal tests", () => { + ruleTester.run(ruleName, rule, { + valid: ['const notVar = 42;'], + invalid: [], + }); + sinon.assert.calledWith(spyRuleTesterIt, 'const notVar = 42;'); + sinon.assert.notCalled(spyRuleTesterItOnly); + }); - // eslint-disable-next-line no-global-assign -- Restore Mocha global - it = originalGlobalIt; - RuleTester.describe = originalRuleTesterDescribe; - RuleTester.it = originalRuleTesterIt; - }); - beforeEach(() => { - ruleTester = new RuleTester(); - }); + it('calls it or itOnly for every test case', () => { + /* + * `RuleTester` doesn't implement test case exclusivity itself. + * Setting `only: true` just causes `RuleTester` to call + * whatever `only()` function is provided by the test framework + * instead of the regular `it()` function. + */ - it("throws an error explaining that the current test framework does not support `only`", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [{ - code: "const notVar = 42;", - only: true - }], - invalid: [] - }); - }, "The current test framework does not support exclusive tests with `only`."); - }); - }); + ruleTester.run(ruleName, rule, { + valid: [ + 'const valid = 42;', + { + code: 'const onlyValid = 42;', + only: true, + }, + ], + invalid: [ + { + code: 'var invalid = 42;', + errors: [/^Bad var/u], + }, + { + code: 'var onlyInvalid = 42;', + errors: [/^Bad var/u], + only: true, + }, + ], }); - describe("test cases", () => { - const ruleName = "no-var"; - const rule = require("./fixtures/no-var"); - - let originalRuleTesterIt; - let spyRuleTesterIt; - let originalRuleTesterItOnly; - let spyRuleTesterItOnly; - - before(() => { - originalRuleTesterIt = RuleTester.it; - spyRuleTesterIt = sinon.spy(); - RuleTester.it = spyRuleTesterIt; - originalRuleTesterItOnly = RuleTester.itOnly; - spyRuleTesterItOnly = sinon.spy(); - RuleTester.itOnly = spyRuleTesterItOnly; - }); - after(() => { - RuleTester.it = originalRuleTesterIt; - RuleTester.itOnly = originalRuleTesterItOnly; - }); - beforeEach(() => { - spyRuleTesterIt.resetHistory(); - spyRuleTesterItOnly.resetHistory(); - ruleTester = new RuleTester(); - }); - - it("isn't called for normal tests", () => { - ruleTester.run(ruleName, rule, { - valid: ["const notVar = 42;"], - invalid: [] - }); - sinon.assert.calledWith(spyRuleTesterIt, "const notVar = 42;"); - sinon.assert.notCalled(spyRuleTesterItOnly); - }); + sinon.assert.calledWith(spyRuleTesterIt, 'const valid = 42;'); + sinon.assert.calledWith(spyRuleTesterItOnly, 'const onlyValid = 42;'); + sinon.assert.calledWith(spyRuleTesterIt, 'var invalid = 42;'); + sinon.assert.calledWith(spyRuleTesterItOnly, 'var onlyInvalid = 42;'); + }); + }); - it("calls it or itOnly for every test case", () => { - - /* - * `RuleTester` doesn't implement test case exclusivity itself. - * Setting `only: true` just causes `RuleTester` to call - * whatever `only()` function is provided by the test framework - * instead of the regular `it()` function. - */ - - ruleTester.run(ruleName, rule, { - valid: [ - "const valid = 42;", - { - code: "const onlyValid = 42;", - only: true - } - ], - invalid: [ - { - code: "var invalid = 42;", - errors: [/^Bad var/u] - }, - { - code: "var onlyInvalid = 42;", - errors: [/^Bad var/u], - only: true - } - ] - }); + describe('static helper wrapper', () => { + it('adds `only` to string test cases', () => { + const test = RuleTester.only('const valid = 42;'); - sinon.assert.calledWith(spyRuleTesterIt, "const valid = 42;"); - sinon.assert.calledWith(spyRuleTesterItOnly, "const onlyValid = 42;"); - sinon.assert.calledWith(spyRuleTesterIt, "var invalid = 42;"); - sinon.assert.calledWith(spyRuleTesterItOnly, "var onlyInvalid = 42;"); - }); + assert.deepStrictEqual(test, { + code: 'const valid = 42;', + only: true, }); + }); - describe("static helper wrapper", () => { - it("adds `only` to string test cases", () => { - const test = RuleTester.only("const valid = 42;"); - - assert.deepStrictEqual(test, { - code: "const valid = 42;", - only: true - }); - }); - - it("adds `only` to object test cases", () => { - const test = RuleTester.only({ code: "const valid = 42;" }); + it('adds `only` to object test cases', () => { + const test = RuleTester.only({ code: 'const valid = 42;' }); - assert.deepStrictEqual(test, { - code: "const valid = 42;", - only: true - }); - }); + assert.deepStrictEqual(test, { + code: 'const valid = 42;', + only: true, }); + }); }); - - it("should not throw an error when everything passes", () => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "Eval(foo)" + }); + + it('should not throw an error when everything passes', () => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['Eval(foo)'], + invalid: [ + { + code: 'eval(foo)', + errors: [{ message: 'eval sucks.', type: 'CallExpression' }], + }, + ], + }); + }); + + it('should throw an error when valid code is invalid', () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['eval(foo)'], + invalid: [ + { + code: 'eval(foo)', + errors: [{ message: 'eval sucks.', type: 'CallExpression' }], + }, + ], + }); + }, /Should have no errors but had 1/u); + }); + + it('should throw an error when valid code is invalid', () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: [{ code: 'eval(foo)' }], + invalid: [ + { + code: 'eval(foo)', + errors: [{ message: 'eval sucks.', type: 'CallExpression' }], + }, + ], + }); + }, /Should have no errors but had 1/u); + }); + + it('should throw an error if invalid code is valid', () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['Eval(foo)'], + invalid: [ + { + code: 'Eval(foo)', + errors: [{ message: 'eval sucks.', type: 'CallExpression' }], + }, + ], + }); + }, /Should have 1 error but had 0/u); + }); + + it('should throw an error when the error message is wrong', () => { + assert.throws( + () => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + // Only the invalid test matters here + valid: ['bar = baz;'], + invalid: [ + { + code: 'var foo = bar;', + errors: [{ message: 'Bad error message.' }], + }, + ], + }); + }, + assertErrorMatches('Bad var.', 'Bad error message.'), + ); + }); + + it('should throw an error when the error message regex does not match', () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [], + invalid: [ + { + code: 'var foo = bar;', + errors: [{ message: /Bad error message/u }], + }, + ], + }); + }, /Expected 'Bad var.' to match \/Bad error message\//u); + }); + + it('should throw an error when the error is not a supported type', () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + // Only the invalid test matters here + valid: ['bar = baz;'], + invalid: [{ code: 'var foo = bar;', errors: [42] }], + }); + }, /Error should be a string, object, or RegExp/u); + }); + + it('should throw an error when any of the errors is not a supported type', () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + // Only the invalid test matters here + valid: ['bar = baz;'], + invalid: [ + { + code: 'var foo = bar; var baz = quux', + errors: [{ type: 'VariableDeclaration' }, null], + }, + ], + }); + }, /Error should be a string, object, or RegExp/u); + }); + + it('should throw an error when the error is a string and it does not match error message', () => { + assert.throws( + () => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + // Only the invalid test matters here + valid: ['bar = baz;'], + invalid: [{ code: 'var foo = bar;', errors: ['Bad error message.'] }], + }); + }, + assertErrorMatches('Bad var.', 'Bad error message.'), + ); + }); + + it('should throw an error when the error is a string and it does not match error message', () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [], + invalid: [{ code: 'var foo = bar;', errors: [/Bad error message/u] }], + }); + }, /Expected 'Bad var.' to match \/Bad error message\//u); + }); + + it('should not throw an error when the error is a string and it matches error message', () => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + // Only the invalid test matters here + valid: ['bar = baz;'], + invalid: [ + { code: 'var foo = bar;', output: ' foo = bar;', errors: ['Bad var.'] }, + ], + }); + }); + + it('should not throw an error when the error is a regex and it matches error message', () => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [], + invalid: [ + { + code: 'var foo = bar;', + output: ' foo = bar;', + errors: [/^Bad var/u], + }, + ], + }); + }); + + it('should throw an error when the error is an object with an unknown property name', () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: ['bar = baz;'], + invalid: [ + { code: 'var foo = bar;', errors: [{ Message: 'Bad var.' }] }, + ], + }); + }, /Invalid error property name 'Message'/u); + }); + + it('should throw an error when any of the errors is an object with an unknown property name', () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: ['bar = baz;'], + invalid: [ + { + code: 'var foo = bar; var baz = quux', + errors: [ + { message: 'Bad var.', type: 'VariableDeclaration' }, + { message: 'Bad var.', typo: 'VariableDeclaration' }, + ], + }, + ], + }); + }, /Invalid error property name 'typo'/u); + }); + + it('should not throw an error when the error is a regex in an object and it matches error message', () => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [], + invalid: [ + { + code: 'var foo = bar;', + output: ' foo = bar;', + errors: [{ message: /^Bad var/u }], + }, + ], + }); + }); + + it("should throw an error when the expected output doesn't match", () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: ['bar = baz;'], + invalid: [ + { + code: 'var foo = bar;', + output: 'foo = bar', + errors: [{ message: 'Bad var.', type: 'VariableDeclaration' }], + }, + ], + }); + }, /Output is incorrect/u); + }); + + it('should use strict equality to compare output', () => { + const replaceProgramWith5Rule = { + meta: { + fixable: 'code', + }, + + create: context => ({ + Program(node) { + context.report({ + node, + message: 'bad', + fix: fixer => fixer.replaceText(node, '5'), + }); + }, + }), + }; + + // Should not throw. + ruleTester.run('foo', replaceProgramWith5Rule, { + valid: [], + invalid: [{ code: 'var foo = bar;', output: '5', errors: 1 }], + }); + + assert.throws(() => { + ruleTester.run('foo', replaceProgramWith5Rule, { + valid: [], + invalid: [{ code: 'var foo = bar;', output: 5, errors: 1 }], + }); + }, /Output is incorrect/u); + }); + + it("should throw an error when the expected output doesn't match and errors is just a number", () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: ['bar = baz;'], + invalid: [{ code: 'var foo = bar;', output: 'foo = bar', errors: 1 }], + }); + }, /Output is incorrect/u); + }); + + it('should not throw an error when the expected output is null and no errors produce output', () => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['bar = baz;'], + invalid: [ + { code: 'eval(x)', errors: 1, output: null }, + { code: 'eval(x); eval(y);', errors: 2, output: null }, + ], + }); + }); + + it('should throw an error when the expected output is null and problems produce output', () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: ['bar = baz;'], + invalid: [{ code: 'var foo = bar;', output: null, errors: 1 }], + }); + }, /Expected no autofixes to be suggested/u); + + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: ['bar = baz;'], + invalid: [ + { + code: 'var foo = bar; var qux = boop;', + output: null, + errors: 2, + }, + ], + }); + }, /Expected no autofixes to be suggested/u); + }); + + it('should throw an error when the expected output is null and only some problems produce output', () => { + assert.throws(() => { + ruleTester.run( + 'fixes-one-problem', + require('./fixtures/fixes-one-problem'), + { + valid: [], + invalid: [{ code: 'foo', output: null, errors: 2 }], + }, + ); + }, /Expected no autofixes to be suggested/u); + }); + + it("should throw an error when the expected output isn't specified and problems produce output", () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: ['bar = baz;'], + invalid: [{ code: 'var foo = bar;', errors: 1 }], + }); + }, "The rule fixed the code. Please add 'output' property."); + }); + + it('should throw an error if invalid code specifies wrong type', () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['Eval(foo)'], + invalid: [ + { + code: 'eval(foo)', + errors: [{ message: 'eval sucks.', type: 'CallExpression2' }], + }, + ], + }); + }, /Error type should be CallExpression2, found CallExpression/u); + }); + + it('should throw an error if invalid code specifies wrong line', () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['Eval(foo)'], + invalid: [ + { + code: 'eval(foo)', + errors: [ + { message: 'eval sucks.', type: 'CallExpression', line: 5 }, + ], + }, + ], + }); + }, /Error line should be 5/u); + }); + + it('should not skip line assertion if line is a falsy value', () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['Eval(foo)'], + invalid: [ + { + code: '\neval(foo)', + errors: [ + { message: 'eval sucks.', type: 'CallExpression', line: 0 }, ], + }, + ], + }); + }, /Error line should be 0/u); + }); + + it('should throw an error if invalid code specifies wrong column', () => { + const wrongColumn = 10, + expectedErrorMessage = 'Error column should be 1'; + + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['Eval(foo)'], + invalid: [ + { + code: 'eval(foo)', + errors: [ + { + message: 'eval sucks.', + column: wrongColumn, + }, + ], + }, + ], + }); + }, expectedErrorMessage); + }); + + it('should throw error for empty error array', () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [], + }, + ], + }, + ); + }, /Invalid cases must have at least one error/u); + }); + + it('should throw error for errors : 0', () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: 0, + }, + ], + }, + ); + }, /Invalid cases must have 'error' value greater than 0/u); + }); + + it('should not skip column assertion if column is a falsy value', () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['Eval(foo)'], + invalid: [ + { + code: 'var foo; eval(foo)', + errors: [{ message: 'eval sucks.', column: 0 }], + }, + ], + }); + }, /Error column should be 0/u); + }); + + it('should throw an error if invalid code specifies wrong endLine', () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: ['bar = baz;'], + invalid: [ + { + code: 'var foo = bar;', + output: 'foo = bar', + errors: [ + { message: 'Bad var.', type: 'VariableDeclaration', endLine: 10 }, + ], + }, + ], + }); + }, 'Error endLine should be 10'); + }); + + it('should throw an error if invalid code specifies wrong endColumn', () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: ['bar = baz;'], + invalid: [ + { + code: 'var foo = bar;', + output: 'foo = bar', + errors: [ + { + message: 'Bad var.', + type: 'VariableDeclaration', + endColumn: 10, + }, + ], + }, + ], + }); + }, 'Error endColumn should be 10'); + }); + + it('should throw an error if invalid code has the wrong number of errors', () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['Eval(foo)'], + invalid: [ + { + code: 'eval(foo)', + errors: [ + { message: 'eval sucks.', type: 'CallExpression' }, + { message: 'eval sucks.', type: 'CallExpression' }, + ], + }, + ], + }); + }, /Should have 2 errors but had 1/u); + }); + + it('should throw an error if invalid code does not have errors', () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['Eval(foo)'], + invalid: [{ code: 'eval(foo)' }], + }); + }, /Did not specify errors for an invalid test of no-eval/u); + }); + + it('should throw an error if invalid code has the wrong explicit number of errors', () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['Eval(foo)'], + invalid: [{ code: 'eval(foo)', errors: 2 }], + }); + }, /Should have 2 errors but had 1/u); + }); + + it("should throw an error if there's a parsing error in a valid test", () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ["1eval('foo')"], + invalid: [{ code: "eval('foo')", errors: [{}] }], + }); + }, /fatal parsing error/iu); + }); + + it("should throw an error if there's a parsing error in an invalid test", () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ["noeval('foo')"], + invalid: [{ code: "1eval('foo')", errors: [{}] }], + }); + }, /fatal parsing error/iu); + }); + + it("should throw an error if there's a parsing error in an invalid test and errors is just a number", () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ["noeval('foo')"], + invalid: [{ code: "1eval('foo')", errors: 1 }], + }); + }, /fatal parsing error/iu); + }); + + // https://github.com/eslint/eslint/issues/4779 + it("should throw an error if there's a parsing error and output doesn't match", () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: [], + invalid: [ + { + code: 'obvious parser error', + output: 'string that doesnt match', + errors: [{}], + }, + ], + }); + }, /fatal parsing error/iu); + }); + + it('should not throw an error if invalid code has at least an expected empty error object', () => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: ['Eval(foo)'], + invalid: [ + { + code: 'eval(foo)', + errors: [{}], + }, + ], + }); + }); + + it('should pass-through the globals config of valid tests to the to rule', () => { + ruleTester.run('no-test-global', require('./fixtures/no-test-global'), { + valid: [ + "var test = 'foo'", + { + code: "var test2 = 'bar'", + globals: { test: true }, + }, + ], + invalid: [{ code: 'bar', errors: 1 }], + }); + }); + + it('should pass-through the globals config of invalid tests to the to rule', () => { + ruleTester.run('no-test-global', require('./fixtures/no-test-global'), { + valid: ["var test = 'foo'"], + invalid: [ + { + code: "var test = 'foo'; var foo = 'bar'", + errors: 1, + }, + { + code: "var test = 'foo'", + globals: { foo: true }, + errors: [{ message: 'Global variable foo should not be used.' }], + }, + ], + }); + }); + + it('should pass-through the settings config to rules', () => { + ruleTester.run('no-test-settings', require('./fixtures/no-test-settings'), { + valid: [ + { + code: "var test = 'bar'", + settings: { test: 1 }, + }, + ], + invalid: [ + { + code: "var test = 'bar'", + settings: { 'no-test': 22 }, + errors: 1, + }, + ], + }); + }); + + it('should pass-through the filename to the rule', () => { + (function () { + ruleTester.run('', require('./fixtures/no-test-filename'), { + valid: [ + { + code: "var foo = 'bar'", + filename: 'somefile.js', + }, + ], + invalid: [ + { + code: "var foo = 'bar'", + errors: [{ message: 'Filename test was not defined.' }], + }, + ], + }); + })(); + }); + + it('should pass-through the options to the rule', () => { + ruleTester.run('no-invalid-args', require('./fixtures/no-invalid-args'), { + valid: [ + { + code: "var foo = 'bar'", + options: [false], + }, + ], + invalid: [ + { + code: "var foo = 'bar'", + options: [true], + errors: [{ message: 'Invalid args' }], + }, + ], + }); + }); + + it('should throw an error if the options are an object', () => { + assert.throws(() => { + ruleTester.run('no-invalid-args', require('./fixtures/no-invalid-args'), { + valid: [ + { + code: 'foo', + options: { ok: true }, + }, + ], + invalid: [], + }); + }, /options must be an array/u); + }); + + it('should throw an error if the options are a number', () => { + assert.throws(() => { + ruleTester.run('no-invalid-args', require('./fixtures/no-invalid-args'), { + valid: [ + { + code: 'foo', + options: 0, + }, + ], + invalid: [], + }); + }, /options must be an array/u); + }); + + it('should pass-through the parser to the rule', () => { + const spy = sinon.spy(ruleTester.linter, 'verify'); + + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: [ + { + code: 'Eval(foo)', + }, + ], + invalid: [ + { + code: 'eval(foo)', + parser: require.resolve('esprima'), + errors: [{ line: 1 }], + }, + ], + }); + assert.strictEqual(spy.args[1][1].parser, require.resolve('esprima')); + }); + + // skipping because it's not something our parser cares about + it.skip('should pass normalized ecmaVersion to the rule', () => { + const reportEcmaVersionRule = { + meta: { + messages: { + ecmaVersionMessage: + 'context.parserOptions.ecmaVersion is {{type}} {{ecmaVersion}}.', + }, + }, + create: context => ({ + Program(node) { + const { ecmaVersion } = context.parserOptions; + + context.report({ + node, + messageId: 'ecmaVersionMessage', + data: { type: typeof ecmaVersion, ecmaVersion }, + }); + }, + }), + }; + + const notEspree = require.resolve('./fixtures/empty-program-parser'); + + ruleTester.run('report-ecma-version', reportEcmaVersionRule, { + valid: [], + invalid: [ + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'undefined', ecmaVersion: 'undefined' }, + }, + ], + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'undefined', ecmaVersion: 'undefined' }, + }, + ], + parserOptions: {}, + }, + { + code: '
', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'undefined', ecmaVersion: 'undefined' }, + }, + ], + parserOptions: { ecmaFeatures: { jsx: true } }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'undefined', ecmaVersion: 'undefined' }, + }, + ], + parser: require.resolve('espree'), + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: '6' }, + }, + ], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: '6' }, + }, + ], + parserOptions: { ecmaVersion: 2015 }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'undefined', ecmaVersion: 'undefined' }, + }, + ], + env: { browser: true }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'undefined', ecmaVersion: 'undefined' }, + }, + ], + env: { es6: false }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: '6' }, + }, + ], + env: { es6: true }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: '8' }, + }, + ], + env: { es6: false, es2017: true }, + }, + { + code: 'let x', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: '6' }, + }, + ], + env: { es6: 'truthy' }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: '8' }, + }, + ], + env: { es2017: true }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: '11' }, + }, + ], + env: { es2020: true }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: '12' }, + }, + ], + env: { es2021: true }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { + type: 'number', + ecmaVersion: String(espree.latestEcmaVersion), + }, + }, + ], + parserOptions: { ecmaVersion: 'latest' }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { + type: 'number', + ecmaVersion: String(espree.latestEcmaVersion), + }, + }, + ], + parser: require.resolve('espree'), + parserOptions: { ecmaVersion: 'latest' }, + }, + { + code: '
', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { + type: 'number', + ecmaVersion: String(espree.latestEcmaVersion), + }, + }, + ], + parserOptions: { ecmaVersion: 'latest', ecmaFeatures: { jsx: true } }, + }, + { + code: "import 'foo'", + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { + type: 'number', + ecmaVersion: String(espree.latestEcmaVersion), + }, + }, + ], + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { + type: 'number', + ecmaVersion: String(espree.latestEcmaVersion), + }, + }, + ], + parserOptions: { ecmaVersion: 'latest' }, + env: { es6: true }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { + type: 'number', + ecmaVersion: String(espree.latestEcmaVersion), + }, + }, + ], + parserOptions: { ecmaVersion: 'latest' }, + env: { es2020: true }, + }, + + // Non-Espree parsers normalize ecmaVersion if it's not "latest" + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'undefined', ecmaVersion: 'undefined' }, + }, + ], + parser: notEspree, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'undefined', ecmaVersion: 'undefined' }, + }, + ], + parser: notEspree, + parserOptions: {}, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: '5' }, + }, + ], + parser: notEspree, + parserOptions: { ecmaVersion: 5 }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: '6' }, + }, + ], + parser: notEspree, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: 6 }, + }, + ], + parser: notEspree, + parserOptions: { ecmaVersion: 2015 }, + }, + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'string', ecmaVersion: 'latest' }, + }, + ], + parser: notEspree, + parserOptions: { ecmaVersion: 'latest' }, + }, + ], + }); + + [{ parserOptions: { ecmaVersion: 6 } }, { env: { es6: true } }].forEach( + options => { + new RuleTester(options).run( + 'report-ecma-version', + reportEcmaVersionRule, + { + valid: [], invalid: [ - { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression" }] } - ] - }); - }); - - it("should throw an error when valid code is invalid", () => { - - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "eval(foo)" + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: '6' }, + }, ], - invalid: [ - { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression" }] } - ] - }); - }, /Should have no errors but had 1/u); - }); - - it("should throw an error when valid code is invalid", () => { - - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - { code: "eval(foo)" } + }, + { + code: '', + parserOptions: {}, + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'number', ecmaVersion: '6' }, + }, ], - invalid: [ - { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression" }] } - ] - }); - }, /Should have no errors but had 1/u); - }); - - it("should throw an error if invalid code is valid", () => { + }, + ], + }, + ); + }, + ); + + new RuleTester({ parser: notEspree }).run( + 'report-ecma-version', + reportEcmaVersionRule, + { + valid: [], + invalid: [ + { + code: '', + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'undefined', ecmaVersion: 'undefined' }, + }, + ], + }, + { + code: '', + parserOptions: { ecmaVersion: 'latest' }, + errors: [ + { + messageId: 'ecmaVersionMessage', + data: { type: 'string', ecmaVersion: 'latest' }, + }, + ], + }, + ], + }, + ); + }); + + it('should pass-through services from parseForESLint to the rule', () => { + const enhancedParserPath = require.resolve('./fixtures/enhanced-parser'); + const disallowHiRule = { + create: context => ({ + Literal(node) { + const disallowed = context.parserServices.test.getMessage(); // returns "Hi!" + + if (node.value === disallowed) { + context.report({ node, message: `Don't use '${disallowed}'` }); + } + }, + }), + }; + + ruleTester.run('no-hi', disallowHiRule, { + valid: [ + { + code: "'Hello!'", + parser: enhancedParserPath, + }, + ], + invalid: [ + { + code: "'Hi!'", + parser: enhancedParserPath, + errors: [{ message: "Don't use 'Hi!'" }], + }, + ], + }); + }); + + it('should prevent invalid options schemas', () => { + assert.throws(() => { + ruleTester.run( + 'no-invalid-schema', + require('./fixtures/no-invalid-schema'), + { + valid: [ + 'var answer = 6 * 7;', + { code: 'var answer = 6 * 7;', options: [] }, + ], + invalid: [ + { + code: 'var answer = 6 * 7;', + options: ['bar'], + errors: [{ message: 'Expected nothing.' }], + }, + ], + }, + ); + }, 'Schema for rule no-invalid-schema is invalid:,\titems: should be object\n\titems[0].enum: should NOT have fewer than 1 items\n\titems: should match some schema in anyOf'); + }); + + it('should prevent schema violations in options', () => { + assert.throws(() => { + ruleTester.run( + 'no-schema-violation', + require('./fixtures/no-schema-violation'), + { + valid: [ + 'var answer = 6 * 7;', + { code: 'var answer = 6 * 7;', options: ['foo'] }, + ], + invalid: [ + { + code: 'var answer = 6 * 7;', + options: ['bar'], + errors: [{ message: 'Expected foo.' }], + }, + ], + }, + ); + }, /Value "bar" should be equal to one of the allowed values./u); + }); + + it('should disallow invalid defaults in rules', () => { + const ruleWithInvalidDefaults = { + meta: { + schema: [ + { + oneOf: [ + { enum: ['foo'] }, + { + type: 'object', + properties: { + foo: { + enum: ['foo', 'bar'], + default: 'foo', + }, + }, + additionalProperties: false, + }, + ], + }, + ], + }, + create: () => ({}), + }; + + assert.throws(() => { + ruleTester.run('invalid-defaults', ruleWithInvalidDefaults, { + valid: [ + { + code: 'foo', + options: [{}], + }, + ], + invalid: [], + }); + }, /Schema for rule invalid-defaults is invalid: default is ignored for: data1\.foo/u); + }); + + it('throw an error when an unknown config option is included', () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: [{ code: 'Eval(foo)', foo: 'bar' }], + invalid: [], + }); + }, /ESLint configuration in rule-tester is invalid./u); + }); + + it('throw an error when an invalid config value is included', () => { + assert.throws(() => { + ruleTester.run('no-eval', require('./fixtures/no-eval'), { + valid: [{ code: 'Eval(foo)', env: ['es6'] }], + invalid: [], + }); + }, /Property "env" is the wrong type./u); + }); - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "Eval(foo)" - ], - invalid: [ - { code: "Eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression" }] } - ] - }); - }, /Should have 1 error but had 0/u); + it('should pass-through the tester config to the rule', () => { + ruleTester = new RuleTester({ + globals: { test: true }, }); - it("should throw an error when the error message is wrong", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - - // Only the invalid test matters here - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "var foo = bar;", errors: [{ message: "Bad error message." }] } - ] - }); - }, assertErrorMatches("Bad var.", "Bad error message.")); + ruleTester.run('no-test-global', require('./fixtures/no-test-global'), { + valid: ["var test = 'foo'", 'var test2 = test'], + invalid: [{ code: 'bar', errors: 1, globals: { foo: true } }], }); + }); - it("should throw an error when the error message regex does not match", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: [{ message: /Bad error message/u }] } - ] - }); - }, /Expected 'Bad var.' to match \/Bad error message\//u); - }); + it('should correctly set the globals configuration', () => { + const config = { globals: { test: true } }; - it("should throw an error when the error is not a supported type", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { + RuleTester.setDefaultConfig(config); + assert( + RuleTester.getDefaultConfig().globals.test, + 'The default config object is incorrect', + ); + }); - // Only the invalid test matters here - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "var foo = bar;", errors: [42] } - ] - }); - }, /Error should be a string, object, or RegExp/u); - }); + it('should correctly reset the global configuration', () => { + const config = { globals: { test: true } }; - it("should throw an error when any of the errors is not a supported type", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { + RuleTester.setDefaultConfig(config); + RuleTester.resetDefaultConfig(); + assert.deepStrictEqual( + RuleTester.getDefaultConfig(), + { parser: require.resolve('@typescript-eslint/parser'), rules: {} }, + 'The default configuration has not reset correctly', + ); + }); - // Only the invalid test matters here - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "var foo = bar; var baz = quux", errors: [{ type: "VariableDeclaration" }, null] } - ] - }); - }, /Error should be a string, object, or RegExp/u); - }); + it('should enforce the global configuration to be an object', () => { + /** + * Set the default config for the rules tester + * @param {Object} config configuration object + * @returns {Function} Function to be executed + * @private + */ + function setConfig(config) { + return function () { + RuleTester.setDefaultConfig(config); + }; + } + assert.throw(setConfig()); + assert.throw(setConfig(1)); + assert.throw(setConfig(3.14)); + assert.throw(setConfig('foo')); + assert.throw(setConfig(null)); + assert.throw(setConfig(true)); + }); + + it('should pass-through the globals config to the tester then to the to rule', () => { + const config = { globals: { test: true } }; + + RuleTester.setDefaultConfig(config); + ruleTester = new RuleTester(); + + ruleTester.run('no-test-global', require('./fixtures/no-test-global'), { + valid: ["var test = 'foo'", 'var test2 = test'], + invalid: [{ code: 'bar', errors: 1, globals: { foo: true } }], + }); + }); + + it('should throw an error if AST was modified', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast'), { + valid: ['var foo = 0;'], + invalid: [], + }); + }, 'Rule should not modify AST.'); + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast'), { + valid: [], + invalid: [{ code: 'var bar = 0;', errors: ['error'] }], + }); + }, 'Rule should not modify AST.'); + }); + + it('should throw an error if AST was modified (at Program)', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast-at-first'), { + valid: ['var foo = 0;'], + invalid: [], + }); + }, 'Rule should not modify AST.'); + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast-at-first'), { + valid: [], + invalid: [{ code: 'var bar = 0;', errors: ['error'] }], + }); + }, 'Rule should not modify AST.'); + }); + + it('should throw an error if AST was modified (at Program:exit)', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), { + valid: ['var foo = 0;'], + invalid: [], + }); + }, 'Rule should not modify AST.'); + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), { + valid: [], + invalid: [{ code: 'var bar = 0;', errors: ['error'] }], + }); + }, 'Rule should not modify AST.'); + }); + + it('should throw an error if rule uses start and end properties on nodes, tokens or comments', () => { + const usesStartEndRule = { + create(context) { + return { + CallExpression(node) { + noop(node.arguments[1].start); + }, + "BinaryExpression[operator='+']"(node) { + noop(node.end); + }, + "UnaryExpression[operator='-']"(node) { + noop(context.sourceCode.getFirstToken(node).start); + }, + ConditionalExpression(node) { + noop(context.sourceCode.getFirstToken(node).end); + }, + BlockStatement(node) { + noop(context.sourceCode.getCommentsInside(node)[0].start); + }, + ObjectExpression(node) { + noop(context.sourceCode.getCommentsInside(node)[0].end); + }, + Decorator(node) { + noop(node.start); + }, + }; + }, + }; - it("should throw an error when the error is a string and it does not match error message", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: ['foo(a, b)'], + invalid: [], + }); + }, 'Use node.range[0] instead of node.start'); + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: [], + invalid: [{ code: 'var a = b * (c + d) / e;', errors: 1 }], + }); + }, 'Use node.range[1] instead of node.end'); + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: [], + invalid: [{ code: 'var a = -b * c;', errors: 1 }], + }); + }, 'Use token.range[0] instead of token.start'); + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: ['var a = b ? c : d;'], + invalid: [], + }); + }, 'Use token.range[1] instead of token.end'); + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: ['function f() { /* comment */ }'], + invalid: [], + }); + }, 'Use token.range[0] instead of token.start'); + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: [], + invalid: [{ code: 'var x = //\n {\n //comment\n //\n}', errors: 1 }], + }); + }, 'Use token.range[1] instead of token.end'); - // Only the invalid test matters here - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "var foo = bar;", errors: ["Bad error message."] } - ] - }); - }, assertErrorMatches("Bad var.", "Bad error message.")); - }); + const enhancedParserPath = require.resolve('./fixtures/enhanced-parser'); - it("should throw an error when the error is a string and it does not match error message", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - - valid: [ - ], - invalid: [ - { code: "var foo = bar;", errors: [/Bad error message/u] } - ] - }); - }, /Expected 'Bad var.' to match \/Bad error message\//u); - }); - - it("should not throw an error when the error is a string and it matches error message", () => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - - // Only the invalid test matters here - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "var foo = bar;", output: " foo = bar;", errors: ["Bad var."] } - ] - }); - }); - - it("should not throw an error when the error is a regex and it matches error message", () => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [], - invalid: [ - { code: "var foo = bar;", output: " foo = bar;", errors: [/^Bad var/u] } - ] - }); - }); - - it("should throw an error when the error is an object with an unknown property name", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "var foo = bar;", errors: [{ Message: "Bad var." }] } - ] - }); - }, /Invalid error property name 'Message'/u); - }); - - it("should throw an error when any of the errors is an object with an unknown property name", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [ - "bar = baz;" - ], - invalid: [ + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: [{ code: 'foo(a, b)', parser: enhancedParserPath }], + invalid: [], + }); + }, 'Use node.range[0] instead of node.start'); + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: [], + invalid: [ + { + code: 'var a = b * (c + d) / e;', + parser: enhancedParserPath, + errors: 1, + }, + ], + }); + }, 'Use node.range[1] instead of node.end'); + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: [], + invalid: [ + { code: 'var a = -b * c;', parser: enhancedParserPath, errors: 1 }, + ], + }); + }, 'Use token.range[0] instead of token.start'); + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: [{ code: 'var a = b ? c : d;', parser: enhancedParserPath }], + invalid: [], + }); + }, 'Use token.range[1] instead of token.end'); + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: [ + { + code: 'function f() { /* comment */ }', + parser: enhancedParserPath, + }, + ], + invalid: [], + }); + }, 'Use token.range[0] instead of token.start'); + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: [], + invalid: [ + { + code: 'var x = //\n {\n //comment\n //\n}', + parser: enhancedParserPath, + errors: 1, + }, + ], + }); + }, 'Use token.range[1] instead of token.end'); + + assert.throws(() => { + ruleTester.run('uses-start-end', usesStartEndRule, { + valid: [ + { + code: '@foo class A {}', + parser: require.resolve('./fixtures/enhanced-parser2'), + }, + ], + invalid: [], + }); + }, 'Use node.range[0] instead of node.start'); + }); + + it('should throw an error if no test scenarios given', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast-at-last')); + }, 'Test Scenarios for rule foo : Could not find test scenario object'); + }); + + it('should throw an error if no acceptable test scenario object is given', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), []); + }, 'Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios\nCould not find any invalid test scenarios'); + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), ''); + }, 'Test Scenarios for rule foo : Could not find test scenario object'); + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), 2); + }, 'Test Scenarios for rule foo : Could not find test scenario object'); + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), {}); + }, 'Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios\nCould not find any invalid test scenarios'); + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), { + valid: [], + }); + }, 'Test Scenarios for rule foo is invalid:\nCould not find any invalid test scenarios'); + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), { + invalid: [], + }); + }, 'Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios'); + }); + + // Nominal message/messageId use cases + it('should assert match if message provided in both test and result.', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/messageId').withMessageOnly, { + valid: [], + invalid: [{ code: 'foo', errors: [{ message: 'something' }] }], + }); + }, /Avoid using variables named/u); + + ruleTester.run('foo', require('./fixtures/messageId').withMessageOnly, { + valid: [], + invalid: [ + { + code: 'foo', + errors: [{ message: "Avoid using variables named 'foo'." }], + }, + ], + }); + }); + + it('should assert match between messageId if provided in both test and result.', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/messageId').withMetaWithData, { + valid: [], + invalid: [{ code: 'foo', errors: [{ messageId: 'unused' }] }], + }); + }, "messageId 'avoidFoo' does not match expected messageId 'unused'."); + + ruleTester.run('foo', require('./fixtures/messageId').withMetaWithData, { + valid: [], + invalid: [{ code: 'foo', errors: [{ messageId: 'avoidFoo' }] }], + }); + }); + it('should assert match between resulting message output if messageId and data provided in both test and result', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/messageId').withMetaWithData, { + valid: [], + invalid: [ + { + code: 'foo', + errors: [{ messageId: 'avoidFoo', data: { name: 'notFoo' } }], + }, + ], + }); + }, 'Hydrated message "Avoid using variables named \'notFoo\'." does not match "Avoid using variables named \'foo\'."'); + }); + + // messageId/message misconfiguration cases + it('should throw if user tests for both message and messageId', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/messageId').withMetaWithData, { + valid: [], + invalid: [ + { + code: 'foo', + errors: [{ message: 'something', messageId: 'avoidFoo' }], + }, + ], + }); + }, "Error should not specify both 'message' and a 'messageId'."); + }); + it("should throw if user tests for messageId but the rule doesn't use the messageId meta syntax.", () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/messageId').withMessageOnly, { + valid: [], + invalid: [{ code: 'foo', errors: [{ messageId: 'avoidFoo' }] }], + }); + }, "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'"); + }); + it("should throw if user tests for messageId not listed in the rule's meta syntax.", () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/messageId').withMetaWithData, { + valid: [], + invalid: [{ code: 'foo', errors: [{ messageId: 'useFoo' }] }], + }); + }, /Invalid messageId 'useFoo'/u); + }); + it('should throw if data provided without messageId.', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/messageId').withMetaWithData, { + valid: [], + invalid: [{ code: 'foo', errors: [{ data: 'something' }] }], + }); + }, "Error must specify 'messageId' if 'data' is used."); + }); + + describe('suggestions', () => { + it('should pass with valid suggestions (tested using desc)', () => { + ruleTester.run( + 'suggestions-basic', + require('./fixtures/suggestions').basic, + { + valid: ['var boo;'], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ { - code: "var foo = bar; var baz = quux", - errors: [ - { message: "Bad var.", type: "VariableDeclaration" }, - { message: "Bad var.", typo: "VariableDeclaration" } - ] - } - ] - }); - }, /Invalid error property name 'typo'/u); - }); - - it("should not throw an error when the error is a regex in an object and it matches error message", () => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [], - invalid: [ - { code: "var foo = bar;", output: " foo = bar;", errors: [{ message: /^Bad var/u }] } - ] - }); - }); - - it("should throw an error when the expected output doesn't match", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "var foo = bar;", output: "foo = bar", errors: [{ message: "Bad var.", type: "VariableDeclaration" }] } - ] - }); - }, /Output is incorrect/u); - }); - - it("should use strict equality to compare output", () => { - const replaceProgramWith5Rule = { - meta: { - fixable: "code" + desc: "Rename identifier 'foo' to 'bar'", + output: 'var bar;', + }, + ], + }, + ], }, - - create: context => ({ - Program(node) { - context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); - } - }) - }; - - // Should not throw. - ruleTester.run("foo", replaceProgramWith5Rule, { - valid: [], - invalid: [ - { code: "var foo = bar;", output: "5", errors: 1 } - ] - }); - - assert.throws(() => { - ruleTester.run("foo", replaceProgramWith5Rule, { - valid: [], - invalid: [ - { code: "var foo = bar;", output: 5, errors: 1 } - ] - }); - }, /Output is incorrect/u); - }); - - it("should throw an error when the expected output doesn't match and errors is just a number", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "var foo = bar;", output: "foo = bar", errors: 1 } - ] - }); - }, /Output is incorrect/u); - }); - - it("should not throw an error when the expected output is null and no errors produce output", () => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "eval(x)", errors: 1, output: null }, - { code: "eval(x); eval(y);", errors: 2, output: null } - ] - }); - }); - - it("should throw an error when the expected output is null and problems produce output", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "var foo = bar;", output: null, errors: 1 } - ] - }); - }, /Expected no autofixes to be suggested/u); - - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [ - "bar = baz;" - ], - invalid: [ + ], + }, + ); + }); + + it('should pass with suggestions on multiple lines', () => { + ruleTester.run( + 'suggestions-basic', + require('./fixtures/suggestions').basic, + { + valid: [], + invalid: [ + { + code: 'function foo() {\n var foo = 1;\n}', + errors: [ + { + suggestions: [ { - code: "var foo = bar; var qux = boop;", - output: null, - errors: 2 - } - ] - }); - }, /Expected no autofixes to be suggested/u); - }); - - it("should throw an error when the expected output is null and only some problems produce output", () => { - assert.throws(() => { - ruleTester.run("fixes-one-problem", require("./fixtures/fixes-one-problem"), { - valid: [], - invalid: [ - { code: "foo", output: null, errors: 2 } - ] - }); - }, /Expected no autofixes to be suggested/u); - }); - - it("should throw an error when the expected output isn't specified and problems produce output", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - }, "The rule fixed the code. Please add 'output' property."); - }); - - it("should throw an error if invalid code specifies wrong type", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "Eval(foo)" - ], - invalid: [ - { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression2" }] } - ] - }); - }, /Error type should be CallExpression2, found CallExpression/u); - }); - - it("should throw an error if invalid code specifies wrong line", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "Eval(foo)" - ], - invalid: [ - { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression", line: 5 }] } - ] - }); - }, /Error line should be 5/u); - }); - - it("should not skip line assertion if line is a falsy value", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "Eval(foo)" - ], - invalid: [ - { code: "\neval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression", line: 0 }] } - ] - }); - }, /Error line should be 0/u); - }); - - it("should throw an error if invalid code specifies wrong column", () => { - const wrongColumn = 10, - expectedErrorMessage = "Error column should be 1"; - - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: ["Eval(foo)"], - invalid: [{ - code: "eval(foo)", - errors: [{ - message: "eval sucks.", - column: wrongColumn - }] - }] - }); - }, expectedErrorMessage); - }); - - it("should throw error for empty error array", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [] - }] - }); - }, /Invalid cases must have at least one error/u); - }); - - it("should throw error for errors : 0", () => { - assert.throws(() => { - ruleTester.run( - "suggestions-messageIds", - require("./fixtures/suggestions") - .withMessageIds, + desc: "Rename identifier 'foo' to 'bar'", + output: 'function bar() {\n var foo = 1;\n}', + }, + ], + }, { - valid: [], - invalid: [ - { - code: "var foo;", - errors: 0 - } - ] - } - ); - }, /Invalid cases must have 'error' value greater than 0/u); - }); - - it("should not skip column assertion if column is a falsy value", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: ["Eval(foo)"], - invalid: [{ - code: "var foo; eval(foo)", - errors: [{ message: "eval sucks.", column: 0 }] - }] - }); - }, /Error column should be 0/u); - }); - - it("should throw an error if invalid code specifies wrong endLine", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "var foo = bar;", output: "foo = bar", errors: [{ message: "Bad var.", type: "VariableDeclaration", endLine: 10 }] } - ] - }); - }, "Error endLine should be 10"); - }); - - it("should throw an error if invalid code specifies wrong endColumn", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [ - "bar = baz;" - ], - invalid: [ - { code: "var foo = bar;", output: "foo = bar", errors: [{ message: "Bad var.", type: "VariableDeclaration", endColumn: 10 }] } - ] - }); - }, "Error endColumn should be 10"); - }); - - it("should throw an error if invalid code has the wrong number of errors", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "Eval(foo)" - ], - invalid: [ + suggestions: [ { - code: "eval(foo)", - errors: [ - { message: "eval sucks.", type: "CallExpression" }, - { message: "eval sucks.", type: "CallExpression" } - ] - } - ] - }); - }, /Should have 2 errors but had 1/u); - }); - - it("should throw an error if invalid code does not have errors", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "Eval(foo)" - ], - invalid: [ - { code: "eval(foo)" } - ] - }); - }, /Did not specify errors for an invalid test of no-eval/u); - }); - - it("should throw an error if invalid code has the wrong explicit number of errors", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "Eval(foo)" - ], - invalid: [ - { code: "eval(foo)", errors: 2 } - ] - }); - }, /Should have 2 errors but had 1/u); - }); - - it("should throw an error if there's a parsing error in a valid test", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "1eval('foo')" - ], - invalid: [ - { code: "eval('foo')", errors: [{}] } - ] - }); - }, /fatal parsing error/iu); - }); - - it("should throw an error if there's a parsing error in an invalid test", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "noeval('foo')" - ], - invalid: [ - { code: "1eval('foo')", errors: [{}] } - ] - }); - }, /fatal parsing error/iu); - }); - - it("should throw an error if there's a parsing error in an invalid test and errors is just a number", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - "noeval('foo')" - ], - invalid: [ - { code: "1eval('foo')", errors: 1 } - ] - }); - }, /fatal parsing error/iu); - }); - - // https://github.com/eslint/eslint/issues/4779 - it("should throw an error if there's a parsing error and output doesn't match", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [], - invalid: [ - { code: "obvious parser error", output: "string that doesnt match", errors: [{}] } - ] - }); - }, /fatal parsing error/iu); - }); - - it("should not throw an error if invalid code has at least an expected empty error object", () => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: ["Eval(foo)"], - invalid: [{ - code: "eval(foo)", - errors: [{}] - }] - }); - }); - - it("should pass-through the globals config of valid tests to the to rule", () => { - ruleTester.run("no-test-global", require("./fixtures/no-test-global"), { - valid: [ - "var test = 'foo'", + desc: "Rename identifier 'foo' to 'bar'", + output: 'function foo() {\n var bar = 1;\n}', + }, + ], + }, + ], + }, + ], + }, + ); + }); + + it('should pass with valid suggestions (tested using messageIds)', () => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ { - code: "var test2 = 'bar'", - globals: { test: true } - } - ], - invalid: [{ code: "bar", errors: 1 }] - }); - }); - - it("should pass-through the globals config of invalid tests to the to rule", () => { - ruleTester.run("no-test-global", require("./fixtures/no-test-global"), { - valid: ["var test = 'foo'"], - invalid: [ + suggestions: [ + { + messageId: 'renameFoo', + output: 'var bar;', + }, + { + messageId: 'renameFoo', + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }); + + it('should pass with valid suggestions (one tested using messageIds, the other using desc)', () => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ { - code: "var test = 'foo'; var foo = 'bar'", - errors: 1 + suggestions: [ + { + messageId: 'renameFoo', + output: 'var bar;', + }, + { + desc: "Rename identifier 'foo' to 'baz'", + output: 'var baz;', + }, + ], }, + ], + }, + ], + }, + ); + }); + + it('should pass with valid suggestions (tested using both desc and messageIds for the same suggestion)', () => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ { - code: "var test = 'foo'", - globals: { foo: true }, - errors: [{ message: "Global variable foo should not be used." }] - } - ] - }); - }); - - it("should pass-through the settings config to rules", () => { - ruleTester.run("no-test-settings", require("./fixtures/no-test-settings"), { - valid: [ + suggestions: [ + { + desc: "Rename identifier 'foo' to 'bar'", + messageId: 'renameFoo', + output: 'var bar;', + }, + { + desc: "Rename identifier 'foo' to 'baz'", + messageId: 'renameFoo', + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }); + + it('should pass with valid suggestions (tested using only desc on a rule that utilizes meta.messages)', () => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ { - code: "var test = 'bar'", settings: { test: 1 } - } - ], - invalid: [ + suggestions: [ + { + desc: "Rename identifier 'foo' to 'bar'", + output: 'var bar;', + }, + { + desc: "Rename identifier 'foo' to 'baz'", + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }); + + it('should pass with valid suggestions (tested using messageIds and data)', () => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ { - code: "var test = 'bar'", settings: { "no-test": 22 }, errors: 1 - } - ] - }); - }); - - it("should pass-through the filename to the rule", () => { - (function() { - ruleTester.run("", require("./fixtures/no-test-filename"), { - valid: [ + suggestions: [ { - code: "var foo = 'bar'", - filename: "somefile.js" - } - ], - invalid: [ + messageId: 'renameFoo', + data: { newName: 'bar' }, + output: 'var bar;', + }, { - code: "var foo = 'bar'", - errors: [ - { message: "Filename test was not defined." } - ] - } - ] - }); - }()); - }); - - it("should pass-through the options to the rule", () => { - ruleTester.run("no-invalid-args", require("./fixtures/no-invalid-args"), { - valid: [ + messageId: 'renameFoo', + data: { newName: 'baz' }, + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }); + + it('should pass when tested using empty suggestion test objects if the array length is correct', () => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ { - code: "var foo = 'bar'", - options: [false] - } - ], - invalid: [ + suggestions: [{}, {}], + }, + ], + }, + ], + }, + ); + }); + + it('should support explicitly expecting no suggestions', () => { + [void 0, null, false, []].forEach(suggestions => { + ruleTester.run('suggestions-basic', require('./fixtures/no-eval'), { + valid: [], + invalid: [ + { + code: "eval('var foo');", + errors: [ { - code: "var foo = 'bar'", - options: [true], - errors: [{ message: "Invalid args" }] - } - ] + suggestions, + }, + ], + }, + ], }); + }); }); - it("should throw an error if the options are an object", () => { - assert.throws(() => { - ruleTester.run("no-invalid-args", require("./fixtures/no-invalid-args"), { - valid: [ - { - code: "foo", - options: { ok: true } - } - ], - invalid: [] - }); - }, /options must be an array/u); - }); - - it("should throw an error if the options are a number", () => { + it('should fail when expecting no suggestions and there are suggestions', () => { + [void 0, null, false, []].forEach(suggestions => { assert.throws(() => { - ruleTester.run("no-invalid-args", require("./fixtures/no-invalid-args"), { - valid: [ + ruleTester.run( + 'suggestions-basic', + require('./fixtures/suggestions').basic, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ { - code: "foo", - options: 0 - } - ], - invalid: [] - }); - }, /options must be an array/u); + suggestions, + }, + ], + }, + ], + }, + ); + }, 'Error should have no suggestions on error with message: "Avoid using identifiers named \'foo\'."'); + }); }); - it("should pass-through the parser to the rule", () => { - const spy = sinon.spy(ruleTester.linter, "verify"); - - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - { - code: "Eval(foo)" - } - ], - invalid: [ + it("should fail when testing for suggestions that don't exist", () => { + assert.throws(() => { + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ { - code: "eval(foo)", - parser: require.resolve("esprima"), - errors: [{ line: 1 }] - } - ] + suggestions: [ + { + messageId: 'this-does-not-exist', + }, + ], + }, + ], + }, + ], }); - assert.strictEqual(spy.args[1][1].parser, require.resolve("esprima")); + }, 'Error should have an array of suggestions. Instead received "undefined" on error with message: "Bad var."'); }); - // skipping because it's not something our parser cares about - it.skip("should pass normalized ecmaVersion to the rule", () => { - const reportEcmaVersionRule = { - meta: { - messages: { - ecmaVersionMessage: "context.parserOptions.ecmaVersion is {{type}} {{ecmaVersion}}." - } - }, - create: context => ({ - Program(node) { - const { ecmaVersion } = context.parserOptions; - - context.report({ - node, - messageId: "ecmaVersionMessage", - data: { type: typeof ecmaVersion, ecmaVersion } - }); - } - }) - }; - - const notEspree = require.resolve("./fixtures/empty-program-parser"); - - ruleTester.run("report-ecma-version", reportEcmaVersionRule, { + it('should fail when there are a different number of suggestions', () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-basic', + require('./fixtures/suggestions').basic, + { valid: [], invalid: [ - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }] - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], - parserOptions: {} - }, - { - code: "
", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], - parserOptions: { ecmaFeatures: { jsx: true } } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], - parser: require.resolve("espree") - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], - parserOptions: { ecmaVersion: 6 } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], - parserOptions: { ecmaVersion: 2015 } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], - env: { browser: true } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], - env: { es6: false } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], - env: { es6: true } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "8" } }], - env: { es6: false, es2017: true } - }, - { - code: "let x", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], - env: { es6: "truthy" } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "8" } }], - env: { es2017: true } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "11" } }], - env: { es2020: true } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "12" } }], - env: { es2021: true } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], - parserOptions: { ecmaVersion: "latest" } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], - parser: require.resolve("espree"), - parserOptions: { ecmaVersion: "latest" } - }, - { - code: "
", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], - parserOptions: { ecmaVersion: "latest", ecmaFeatures: { jsx: true } } - }, - { - code: "import 'foo'", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], - parserOptions: { ecmaVersion: "latest", sourceType: "module" } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], - parserOptions: { ecmaVersion: "latest" }, - env: { es6: true } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], - parserOptions: { ecmaVersion: "latest" }, - env: { es2020: true } - }, - - // Non-Espree parsers normalize ecmaVersion if it's not "latest" - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], - parser: notEspree - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], - parser: notEspree, - parserOptions: {} - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "5" } }], - parser: notEspree, - parserOptions: { ecmaVersion: 5 } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], - parser: notEspree, - parserOptions: { ecmaVersion: 6 } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: 6 } }], - parser: notEspree, - parserOptions: { ecmaVersion: 2015 } - }, - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "string", ecmaVersion: "latest" } }], - parser: notEspree, - parserOptions: { ecmaVersion: "latest" } - } - ] - }); - - [{ parserOptions: { ecmaVersion: 6 } }, { env: { es6: true } }].forEach(options => { - new RuleTester(options).run("report-ecma-version", reportEcmaVersionRule, { - valid: [], - invalid: [ - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }] - }, - { - code: "", - parserOptions: {}, - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }] - } - ] - }); - }); - - new RuleTester({ parser: notEspree }).run("report-ecma-version", reportEcmaVersionRule, { - valid: [], - invalid: [ - { - code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }] - }, - { - code: "", - parserOptions: { ecmaVersion: "latest" }, - errors: [{ messageId: "ecmaVersionMessage", data: { type: "string", ecmaVersion: "latest" } }] - } - ] - }); - }); - - it("should pass-through services from parseForESLint to the rule", () => { - const enhancedParserPath = require.resolve("./fixtures/enhanced-parser"); - const disallowHiRule = { - create: context => ({ - Literal(node) { - const disallowed = context.parserServices.test.getMessage(); // returns "Hi!" - - if (node.value === disallowed) { - context.report({ node, message: `Don't use '${disallowed}'` }); - } - } - }) - }; - - ruleTester.run("no-hi", disallowHiRule, { - valid: [ - { - code: "'Hello!'", - parser: enhancedParserPath - } - ], - invalid: [ - { - code: "'Hi!'", - parser: enhancedParserPath, - errors: [{ message: "Don't use 'Hi!'" }] - } - ] - }); - }); - - it("should prevent invalid options schemas", () => { - assert.throws(() => { - ruleTester.run("no-invalid-schema", require("./fixtures/no-invalid-schema"), { - valid: [ - "var answer = 6 * 7;", - { code: "var answer = 6 * 7;", options: [] } - ], - invalid: [ - { code: "var answer = 6 * 7;", options: ["bar"], errors: [{ message: "Expected nothing." }] } - ] - }); - }, "Schema for rule no-invalid-schema is invalid:,\titems: should be object\n\titems[0].enum: should NOT have fewer than 1 items\n\titems: should match some schema in anyOf"); - - }); - - it("should prevent schema violations in options", () => { - assert.throws(() => { - ruleTester.run("no-schema-violation", require("./fixtures/no-schema-violation"), { - valid: [ - "var answer = 6 * 7;", - { code: "var answer = 6 * 7;", options: ["foo"] } + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + desc: "Rename identifier 'foo' to 'bar'", + output: 'var bar;', + }, + { + desc: "Rename identifier 'foo' to 'baz'", + output: 'var baz;', + }, + ], + }, ], - invalid: [ - { code: "var answer = 6 * 7;", options: ["bar"], errors: [{ message: "Expected foo." }] } - ] - }); - }, /Value "bar" should be equal to one of the allowed values./u); - + }, + ], + }, + ); + }, 'Error should have 2 suggestions. Instead found 1 suggestions'); }); - it("should disallow invalid defaults in rules", () => { - const ruleWithInvalidDefaults = { - meta: { - schema: [ - { - oneOf: [ - { enum: ["foo"] }, - { - type: "object", - properties: { - foo: { - enum: ["foo", "bar"], - default: "foo" - } - }, - additionalProperties: false - } - ] - } - ] + it('should throw if suggestion fix made a syntax error.', () => { + assert.throw(() => { + ruleTester.run( + 'foo', + { + meta: { hasSuggestions: true }, + create(context) { + return { + Identifier(node) { + context.report({ + node, + message: 'make a syntax error', + suggest: [ + { + desc: 'make a syntax error', + fix(fixer) { + return fixer.replaceText(node, 'one two'); + }, + }, + ], + }); + }, + }; }, - create: () => ({}) - }; - - assert.throws(() => { - ruleTester.run("invalid-defaults", ruleWithInvalidDefaults, { - valid: [ - { - code: "foo", - options: [{}] - } - ], - invalid: [] - }); - }, /Schema for rule invalid-defaults is invalid: default is ignored for: data1\.foo/u); - }); - - it("throw an error when an unknown config option is included", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - { code: "Eval(foo)", foo: "bar" } - ], - invalid: [] - }); - }, /ESLint configuration in rule-tester is invalid./u); - }); - - it("throw an error when an invalid config value is included", () => { - assert.throws(() => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: [ - { code: "Eval(foo)", env: ["es6"] } + }, + { + valid: [''], + invalid: [ + { + code: 'one()', + errors: [ + { + message: 'make a syntax error', + suggestions: [ + { + desc: 'make a syntax error', + output: 'one two()', + }, + ], + }, ], - invalid: [] - }); - }, /Property "env" is the wrong type./u); - }); - - it("should pass-through the tester config to the rule", () => { - ruleTester = new RuleTester({ - globals: { test: true } - }); - - ruleTester.run("no-test-global", require("./fixtures/no-test-global"), { - valid: [ - "var test = 'foo'", - "var test2 = test" + }, ], - invalid: [{ code: "bar", errors: 1, globals: { foo: true } }] - }); - }); - - it("should correctly set the globals configuration", () => { - const config = { globals: { test: true } }; - - RuleTester.setDefaultConfig(config); - assert( - RuleTester.getDefaultConfig().globals.test, - "The default config object is incorrect" + }, ); + }, /A fatal parsing error occurred in suggestion fix\.\nError: .+\nSuggestion output:\n.+/u); }); - it("should correctly reset the global configuration", () => { - const config = { globals: { test: true } }; - - RuleTester.setDefaultConfig(config); - RuleTester.resetDefaultConfig(); - assert.deepStrictEqual( - RuleTester.getDefaultConfig(), - { parser: require.resolve('@typescript-eslint/parser'), rules: {} }, - "The default configuration has not reset correctly" - ); - }); - - it("should enforce the global configuration to be an object", () => { - - /** - * Set the default config for the rules tester - * @param {Object} config configuration object - * @returns {Function} Function to be executed - * @private - */ - function setConfig(config) { - return function() { - RuleTester.setDefaultConfig(config); - }; - } - assert.throw(setConfig()); - assert.throw(setConfig(1)); - assert.throw(setConfig(3.14)); - assert.throw(setConfig("foo")); - assert.throw(setConfig(null)); - assert.throw(setConfig(true)); - }); - - it("should pass-through the globals config to the tester then to the to rule", () => { - const config = { globals: { test: true } }; - - RuleTester.setDefaultConfig(config); - ruleTester = new RuleTester(); - - ruleTester.run("no-test-global", require("./fixtures/no-test-global"), { - valid: [ - "var test = 'foo'", - "var test2 = test" - ], - invalid: [{ code: "bar", errors: 1, globals: { foo: true } }] - }); - }); - - it("should throw an error if AST was modified", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast"), { - valid: [ - "var foo = 0;" - ], - invalid: [] - }); - }, "Rule should not modify AST."); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast"), { - valid: [], - invalid: [ - { code: "var bar = 0;", errors: ["error"] } - ] - }); - }, "Rule should not modify AST."); - }); - - it("should throw an error if AST was modified (at Program)", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast-at-first"), { - valid: [ - "var foo = 0;" - ], - invalid: [] - }); - }, "Rule should not modify AST."); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast-at-first"), { - valid: [], - invalid: [ - { code: "var bar = 0;", errors: ["error"] } - ] - }); - }, "Rule should not modify AST."); - }); - - it("should throw an error if AST was modified (at Program:exit)", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), { - valid: [ - "var foo = 0;" - ], - invalid: [] - }); - }, "Rule should not modify AST."); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), { - valid: [], - invalid: [ - { code: "var bar = 0;", errors: ["error"] } - ] - }); - }, "Rule should not modify AST."); - }); - - it("should throw an error if rule uses start and end properties on nodes, tokens or comments", () => { - const usesStartEndRule = { - create(context) { - return { - CallExpression(node) { - noop(node.arguments[1].start); - }, - "BinaryExpression[operator='+']"(node) { - noop(node.end); - }, - "UnaryExpression[operator='-']"(node) { - noop(context.sourceCode.getFirstToken(node).start); - }, - ConditionalExpression(node) { - noop(context.sourceCode.getFirstToken(node).end); - }, - BlockStatement(node) { - noop(context.sourceCode.getCommentsInside(node)[0].start); - }, - ObjectExpression(node) { - noop(context.sourceCode.getCommentsInside(node)[0].end); - }, - Decorator(node) { - noop(node.start); - } - }; - } - }; - - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: ["foo(a, b)"], - invalid: [] - }); - }, "Use node.range[0] instead of node.start"); - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: [], - invalid: [{ code: "var a = b * (c + d) / e;", errors: 1 }] - }); - }, "Use node.range[1] instead of node.end"); - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: [], - invalid: [{ code: "var a = -b * c;", errors: 1 }] - }); - }, "Use token.range[0] instead of token.start"); - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: ["var a = b ? c : d;"], - invalid: [] - }); - }, "Use token.range[1] instead of token.end"); - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: ["function f() { /* comment */ }"], - invalid: [] - }); - }, "Use token.range[0] instead of token.start"); - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: [], - invalid: [{ code: "var x = //\n {\n //comment\n //\n}", errors: 1 }] - }); - }, "Use token.range[1] instead of token.end"); - - const enhancedParserPath = require.resolve("./fixtures/enhanced-parser"); - - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: [{ code: "foo(a, b)", parser: enhancedParserPath }], - invalid: [] - }); - }, "Use node.range[0] instead of node.start"); - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: [], - invalid: [{ code: "var a = b * (c + d) / e;", parser: enhancedParserPath, errors: 1 }] - }); - }, "Use node.range[1] instead of node.end"); - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: [], - invalid: [{ code: "var a = -b * c;", parser: enhancedParserPath, errors: 1 }] - }); - }, "Use token.range[0] instead of token.start"); - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: [{ code: "var a = b ? c : d;", parser: enhancedParserPath }], - invalid: [] - }); - }, "Use token.range[1] instead of token.end"); - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: [{ code: "function f() { /* comment */ }", parser: enhancedParserPath }], - invalid: [] - }); - }, "Use token.range[0] instead of token.start"); - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: [], - invalid: [{ code: "var x = //\n {\n //comment\n //\n}", parser: enhancedParserPath, errors: 1 }] - }); - }, "Use token.range[1] instead of token.end"); - - assert.throws(() => { - ruleTester.run("uses-start-end", usesStartEndRule, { - valid: [{ code: "@foo class A {}", parser: require.resolve("./fixtures/enhanced-parser2") }], - invalid: [] - }); - }, "Use node.range[0] instead of node.start"); - }); - - it("should throw an error if no test scenarios given", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast-at-last")); - }, "Test Scenarios for rule foo : Could not find test scenario object"); - }); - - it("should throw an error if no acceptable test scenario object is given", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), []); - }, "Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios\nCould not find any invalid test scenarios"); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), ""); - }, "Test Scenarios for rule foo : Could not find test scenario object"); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), 2); - }, "Test Scenarios for rule foo : Could not find test scenario object"); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), {}); - }, "Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios\nCould not find any invalid test scenarios"); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), { - valid: [] - }); - }, "Test Scenarios for rule foo is invalid:\nCould not find any invalid test scenarios"); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), { - invalid: [] - }); - }, "Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios"); - }); - - // Nominal message/messageId use cases - it("should assert match if message provided in both test and result.", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMessageOnly, { - valid: [], - invalid: [{ code: "foo", errors: [{ message: "something" }] }] - }); - }, /Avoid using variables named/u); - - ruleTester.run("foo", require("./fixtures/messageId").withMessageOnly, { + it("should throw if the suggestion description doesn't match", () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-basic', + require('./fixtures/suggestions').basic, + { valid: [], - invalid: [{ code: "foo", errors: [{ message: "Avoid using variables named 'foo'." }] }] - }); + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + desc: 'not right', + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }, 'Error Suggestion at index 0 : desc should be "not right" but got "Rename identifier \'foo\' to \'bar\'" instead.'); }); - it("should assert match between messageId if provided in both test and result.", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "unused" }] }] - }); - }, "messageId 'avoidFoo' does not match expected messageId 'unused'."); - - ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, { + it("should throw if the suggestion description doesn't match (although messageIds match)", () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] - }); - }); - it("should assert match between resulting message output if messageId and data provided in both test and result", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "notFoo" } }] }] - }); - }, "Hydrated message \"Avoid using variables named 'notFoo'.\" does not match \"Avoid using variables named 'foo'.\""); - }); - - // messageId/message misconfiguration cases - it("should throw if user tests for both message and messageId", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, { - valid: [], - invalid: [{ code: "foo", errors: [{ message: "something", messageId: "avoidFoo" }] }] - }); - }, "Error should not specify both 'message' and a 'messageId'."); - }); - it("should throw if user tests for messageId but the rule doesn't use the messageId meta syntax.", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMessageOnly, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] - }); - }, "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'"); - }); - it("should throw if user tests for messageId not listed in the rule's meta syntax.", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "useFoo" }] }] - }); - }, /Invalid messageId 'useFoo'/u); - }); - it("should throw if data provided without messageId.", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, { - valid: [], - invalid: [{ code: "foo", errors: [{ data: "something" }] }] - }); - }, "Error must specify 'messageId' if 'data' is used."); - }); - - describe("suggestions", () => { - it("should pass with valid suggestions (tested using desc)", () => { - ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { - valid: [ - "var boo;" - ], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - output: "var bar;" - }] - }] - }] - }); - }); - - it("should pass with suggestions on multiple lines", () => { - ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { - valid: [], - invalid: [ - { - code: "function foo() {\n var foo = 1;\n}", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - output: "function bar() {\n var foo = 1;\n}" - }] - }, { - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - output: "function foo() {\n var bar = 1;\n}" - }] - }] - } - ] - }); - }); - - it("should pass with valid suggestions (tested using messageIds)", () => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - messageId: "renameFoo", - output: "var bar;" - }, { - messageId: "renameFoo", - output: "var baz;" - }] - }] - }] - }); - }); - - it("should pass with valid suggestions (one tested using messageIds, the other using desc)", () => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename identifier 'foo' to 'baz'", - output: "var baz;" - }] - }] - }] - }); - }); - - it("should pass with valid suggestions (tested using both desc and messageIds for the same suggestion)", () => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename identifier 'foo' to 'baz'", - messageId: "renameFoo", - output: "var baz;" - }] - }] - }] - }); - }); - - it("should pass with valid suggestions (tested using only desc on a rule that utilizes meta.messages)", () => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - output: "var bar;" - }, { - desc: "Rename identifier 'foo' to 'baz'", - output: "var baz;" - }] - }] - }] - }); - }); - - it("should pass with valid suggestions (tested using messageIds and data)", () => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - messageId: "renameFoo", - data: { newName: "bar" }, - output: "var bar;" - }, { - messageId: "renameFoo", - data: { newName: "baz" }, - output: "var baz;" - }] - }] - }] - }); - }); - - - it("should pass when tested using empty suggestion test objects if the array length is correct", () => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{}, {}] - }] - }] - }); - }); - - it("should support explicitly expecting no suggestions", () => { - [void 0, null, false, []].forEach(suggestions => { - ruleTester.run("suggestions-basic", require("./fixtures/no-eval"), { - valid: [], - invalid: [{ - code: "eval('var foo');", - errors: [{ - suggestions - }] - }] - }); - }); - }); - - it("should fail when expecting no suggestions and there are suggestions", () => { - [void 0, null, false, []].forEach(suggestions => { - assert.throws(() => { - ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions - }] - }] - }); - }, "Error should have no suggestions on error with message: \"Avoid using identifiers named 'foo'.\""); - }); - }); - - it("should fail when testing for suggestions that don't exist", () => { - assert.throws(() => { - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - messageId: "this-does-not-exist" - }] - }] - }] - }); - }, "Error should have an array of suggestions. Instead received \"undefined\" on error with message: \"Bad var.\""); - }); - - it("should fail when there are a different number of suggestions", () => { - assert.throws(() => { - ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - output: "var bar;" - }, { - desc: "Rename identifier 'foo' to 'baz'", - output: "var baz;" - }] - }] - }] - }); - }, "Error should have 2 suggestions. Instead found 1 suggestions"); - }); - - it("should throw if the suggestion description doesn't match", () => { - assert.throws(() => { - ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "not right", - output: "var baz;" - }] - }] - }] - }); - }, "Error Suggestion at index 0 : desc should be \"not right\" but got \"Rename identifier 'foo' to 'bar'\" instead."); - }); - - it("should throw if the suggestion description doesn't match (although messageIds match)", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename id 'foo' to 'baz'", - messageId: "renameFoo", - output: "var baz;" - }] - }] - }] - }); - }, "Error Suggestion at index 1 : desc should be \"Rename id 'foo' to 'baz'\" but got \"Rename identifier 'foo' to 'baz'\" instead."); - }); - - it("should throw if the suggestion messageId doesn't match", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - messageId: "unused", - output: "var bar;" - }, { - messageId: "renameFoo", - output: "var baz;" - }] - }] - }] - }); - }, "Error Suggestion at index 0 : messageId should be 'unused' but got 'renameFoo' instead."); - }); - - it("should throw if the suggestion messageId doesn't match (although descriptions match)", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename identifier 'foo' to 'baz'", - messageId: "avoidFoo", - output: "var baz;" - }] - }] - }] - }); - }, "Error Suggestion at index 1 : messageId should be 'avoidFoo' but got 'renameFoo' instead."); - }); - - it("should throw if test specifies messageId for a rule that doesn't have meta.messages", () => { - assert.throws(() => { - ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - messageId: "renameFoo", - output: "var bar;" - }] - }] - }] - }); - }, "Error Suggestion at index 0 : Test can not use 'messageId' if rule under test doesn't define 'meta.messages'."); - }); - - it("should throw if test specifies messageId that doesn't exist in the rule's meta.messages", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - messageId: "renameFoo", - output: "var bar;" - }, { - messageId: "removeFoo", - output: "var baz;" - }] - }] - }] - }); - }, "Error Suggestion at index 1 : Test has invalid messageId 'removeFoo', the rule under test allows only one of ['avoidFoo', 'unused', 'renameFoo']."); - }); - - it("should throw if hydrated desc doesn't match (wrong data value)", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - messageId: "renameFoo", - data: { newName: "car" }, - output: "var bar;" - }, { - messageId: "renameFoo", - data: { newName: "baz" }, - output: "var baz;" - }] - }] - }] - }); - }, "Error Suggestion at index 0 : Hydrated test desc \"Rename identifier 'foo' to 'car'\" does not match received desc \"Rename identifier 'foo' to 'bar'\"."); - }); - - it("should throw if hydrated desc doesn't match (wrong data key)", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - messageId: "renameFoo", - data: { newName: "bar" }, - output: "var bar;" - }, { - messageId: "renameFoo", - data: { name: "baz" }, - output: "var baz;" - }] - }] - }] - }); - }, "Error Suggestion at index 1 : Hydrated test desc \"Rename identifier 'foo' to '{{ newName }}'\" does not match received desc \"Rename identifier 'foo' to 'baz'\"."); - }); - - it("should throw if test specifies both desc and data", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - data: { newName: "bar" }, - output: "var bar;" - }, { - messageId: "renameFoo", - data: { newName: "baz" }, - output: "var baz;" - }] - }] - }] - }); - }, "Error Suggestion at index 0 : Test should not specify both 'desc' and 'data'."); - }); - - it("should throw if test uses data but doesn't specify messageId", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - messageId: "renameFoo", - data: { newName: "bar" }, - output: "var bar;" - }, { - data: { newName: "baz" }, - output: "var baz;" - }] - }] - }] - }); - }, "Error Suggestion at index 1 : Test must specify 'messageId' if 'data' is used."); - }); - - it("should throw if the resulting suggestion output doesn't match", () => { - assert.throws(() => { - ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - output: "var baz;" - }] - }] - }] - }); - }, "Expected the applied suggestion fix to match the test suggestion output"); - }); - - it("should fail when specified suggestion isn't an object", () => { - assert.throws(() => { - ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [null] - }] - }] - }); - }, "Test suggestion in 'suggestions' array must be an object."); - - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [ - { - messageId: "renameFoo", - output: "var bar;" - }, - "Rename identifier 'foo' to 'baz'" - ] - }] - }] - }); - }, "Test suggestion in 'suggestions' array must be an object."); - }); - - it("should fail when the suggestion is an object with an unknown property name", () => { - assert.throws(() => { - ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { - valid: [ - "var boo;" + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + desc: "Rename identifier 'foo' to 'bar'", + messageId: 'renameFoo', + output: 'var bar;', + }, + { + desc: "Rename id 'foo' to 'baz'", + messageId: 'renameFoo', + output: 'var baz;', + }, ], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - message: "Rename identifier 'foo' to 'bar'" - }] - }] - }] - }); - }, /Invalid suggestion property name 'message'/u); - }); - - it("should fail when any of the suggestions is an object with an unknown property name", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - messageId: "renameFoo", - output: "var bar;" - }, { - messageId: "renameFoo", - outpt: "var baz;" - }] - }] - }] - }); - }, /Invalid suggestion property name 'outpt'/u); - }); - - it("should fail if a rule produces two suggestions with the same description", () => { - assert.throws(() => { - ruleTester.run("suggestions-with-duplicate-descriptions", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateDescriptions, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - }, "Suggestion message 'Rename 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); - }); - - it("should fail if a rule produces two suggestions with the same messageId without data", () => { - assert.throws(() => { - ruleTester.run("suggestions-with-duplicate-messageids-no-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsNoData, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - }, "Suggestion message 'Rename identifier' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); - }); - - it("should fail if a rule produces two suggestions with the same messageId with data", () => { - assert.throws(() => { - ruleTester.run("suggestions-with-duplicate-messageids-with-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsWithData, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - }, "Suggestion message 'Rename identifier 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); - }); - - it("should throw an error if a rule that doesn't have `meta.hasSuggestions` enabled produces suggestions", () => { - assert.throws(() => { - ruleTester.run("suggestions-missing-hasSuggestions-property", require("./fixtures/suggestions").withoutHasSuggestionsProperty, { - valid: [], - invalid: [ - { code: "var foo = bar;", output: "5", errors: 1 } - ] - }); - }, "Rules with suggestions must set the `meta.hasSuggestions` property to `true`."); - }); - }); - - 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/latest/extend/custom-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/latest/extend/custom-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/latest/extend/custom-rules#options-schemas", - "DeprecationWarning" - ] - ); - }); + }, + ], + }, + ], + }, + ); + }, "Error Suggestion at index 1 : desc should be \"Rename id 'foo' to 'baz'\" but got \"Rename identifier 'foo' to 'baz'\" instead."); + }); - it("should log a deprecation warning when schema is `undefined`", () => { - const ruleWithUndefinedSchema = { - meta: { - type: "problem", - // eslint-disable-next-line no-undefined -- intentionally added for test case - schema: undefined - }, - create(context) { - return { - Program(node) { - context.report({ node, message: "bad" }); - } - }; - } - }; + it("should throw if the suggestion messageId doesn't match", () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + messageId: 'unused', + output: 'var bar;', + }, + { + messageId: 'renameFoo', + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }, "Error Suggestion at index 0 : messageId should be 'unused' but got 'renameFoo' instead."); + }); - ruleTester.run("rule-with-undefined-schema", ruleWithUndefinedSchema, { - valid: [], - invalid: [ - { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } - ] - }); + it("should throw if the suggestion messageId doesn't match (although descriptions match)", () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + desc: "Rename identifier 'foo' to 'bar'", + messageId: 'renameFoo', + output: 'var bar;', + }, + { + desc: "Rename identifier 'foo' to 'baz'", + messageId: 'avoidFoo', + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }, "Error Suggestion at index 1 : messageId should be 'avoidFoo' but got 'renameFoo' instead."); + }); - 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/latest/extend/custom-rules#options-schemas", - "DeprecationWarning" - ] - ); - }); + it("should throw if test specifies messageId for a rule that doesn't have meta.messages", () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-basic', + require('./fixtures/suggestions').basic, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + messageId: 'renameFoo', + output: 'var bar;', + }, + ], + }, + ], + }, + ], + }, + ); + }, "Error Suggestion at index 0 : Test can not use 'messageId' if rule under test doesn't define 'meta.messages'."); + }); - 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" }); - } - }; - } - }; + it("should throw if test specifies messageId that doesn't exist in the rule's meta.messages", () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + messageId: 'renameFoo', + output: 'var bar;', + }, + { + messageId: 'removeFoo', + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }, "Error Suggestion at index 1 : Test has invalid messageId 'removeFoo', the rule under test allows only one of ['avoidFoo', 'unused', 'renameFoo']."); + }); - ruleTester.run("rule-with-null-schema", ruleWithNullSchema, { - valid: [], - invalid: [ - { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } - ] - }); + it("should throw if hydrated desc doesn't match (wrong data value)", () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + messageId: 'renameFoo', + data: { newName: 'car' }, + output: 'var bar;', + }, + { + messageId: 'renameFoo', + data: { newName: 'baz' }, + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }, "Error Suggestion at index 0 : Hydrated test desc \"Rename identifier 'foo' to 'car'\" does not match received desc \"Rename identifier 'foo' to 'bar'\"."); + }); - 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/latest/extend/custom-rules#options-schemas", - "DeprecationWarning" - ] - ); - }); + it("should throw if hydrated desc doesn't match (wrong data key)", () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + messageId: 'renameFoo', + data: { newName: 'bar' }, + output: 'var bar;', + }, + { + messageId: 'renameFoo', + data: { name: 'baz' }, + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }, "Error Suggestion at index 1 : Hydrated test desc \"Rename identifier 'foo' to '{{ newName }}'\" does not match received desc \"Rename identifier 'foo' to 'baz'\"."); + }); - 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" }); - } - }; - } - }; + it('should throw if test specifies both desc and data', () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + desc: "Rename identifier 'foo' to 'bar'", + messageId: 'renameFoo', + data: { newName: 'bar' }, + output: 'var bar;', + }, + { + messageId: 'renameFoo', + data: { newName: 'baz' }, + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }, "Error Suggestion at index 0 : Test should not specify both 'desc' and 'data'."); + }); - ruleTester.run("rule-with-no-options", ruleWithEmptySchema, { - valid: [], - invalid: [{ code: "var foo = bar;", errors: 1 }] - }); + it("should throw if test uses data but doesn't specify messageId", () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + messageId: 'renameFoo', + data: { newName: 'bar' }, + output: 'var bar;', + }, + { + data: { newName: 'baz' }, + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }, "Error Suggestion at index 1 : Test must specify 'messageId' if 'data' is used."); + }); - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); - }); + it("should throw if the resulting suggestion output doesn't match", () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-basic', + require('./fixtures/suggestions').basic, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + desc: "Rename identifier 'foo' to 'bar'", + output: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }, 'Expected the applied suggestion fix to match the test suggestion output'); + }); - 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 } - ] - }); + it("should fail when specified suggestion isn't an object", () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-basic', + require('./fixtures/suggestions').basic, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [null], + }, + ], + }, + ], + }, + ); + }, "Test suggestion in 'suggestions' array must be an object."); - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); - }); + assert.throws(() => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + messageId: 'renameFoo', + output: 'var bar;', + }, + "Rename identifier 'foo' to 'baz'", + ], + }, + ], + }, + ], + }, + ); + }, "Test suggestion in 'suggestions' array must be an object."); + }); - 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" }); - } - }; - } - }; + it('should fail when the suggestion is an object with an unknown property name', () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-basic', + require('./fixtures/suggestions').basic, + { + valid: ['var boo;'], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + message: "Rename identifier 'foo' to 'bar'", + }, + ], + }, + ], + }, + ], + }, + ); + }, /Invalid suggestion property name 'message'/u); + }); - ruleTester.run("rule-with-schema", ruleWithSchema, { - valid: [], - invalid: [ - { code: "var foo = bar;", options: [true], errors: 1 } - ] - }); + it('should fail when any of the suggestions is an object with an unknown property name', () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-messageIds', + require('./fixtures/suggestions').withMessageIds, + { + valid: [], + invalid: [ + { + code: 'var foo;', + errors: [ + { + suggestions: [ + { + messageId: 'renameFoo', + output: 'var bar;', + }, + { + messageId: 'renameFoo', + outpt: 'var baz;', + }, + ], + }, + ], + }, + ], + }, + ); + }, /Invalid suggestion property name 'outpt'/u); + }); - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); - }); + it('should fail if a rule produces two suggestions with the same description', () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-with-duplicate-descriptions', + require('../../fixtures/testers/rule-tester/suggestions') + .withDuplicateDescriptions, + { + valid: [], + invalid: [{ code: 'var foo = bar;', errors: 1 }], + }, + ); + }, "Suggestion message 'Rename 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); - 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 } - ] - }); + it('should fail if a rule produces two suggestions with the same messageId without data', () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-with-duplicate-messageids-no-data', + require('../../fixtures/testers/rule-tester/suggestions') + .withDuplicateMessageIdsNoData, + { + valid: [], + invalid: [{ code: 'var foo = bar;', errors: 1 }], + }, + ); + }, "Suggestion message 'Rename identifier' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); - }); + it('should fail if a rule produces two suggestions with the same messageId with data', () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-with-duplicate-messageids-with-data', + require('../../fixtures/testers/rule-tester/suggestions') + .withDuplicateMessageIdsWithData, + { + valid: [], + invalid: [{ code: 'var foo = bar;', errors: 1 }], + }, + ); + }, "Suggestion message 'Rename identifier 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); - 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 } - ] - }); + it("should throw an error if a rule that doesn't have `meta.hasSuggestions` enabled produces suggestions", () => { + assert.throws(() => { + ruleTester.run( + 'suggestions-missing-hasSuggestions-property', + require('./fixtures/suggestions').withoutHasSuggestionsProperty, + { + valid: [], + invalid: [{ code: 'var foo = bar;', output: '5', errors: 1 }], + }, + ); + }, 'Rules with suggestions must set the `meta.hasSuggestions` property to `true`.'); + }); + }); + + 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' }); + }, + }; + }, + }; - 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 } - ] - }); + beforeEach(() => { + processStub = sinon.stub(process, 'emitWarning'); + }); - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); - }); + afterEach(() => { + processStub.restore(); }); - /** - * Asserts that a particular value will be emitted from an EventEmitter. - * @param {EventEmitter} emitter The emitter that should emit a value - * @param {string} emitType The type of emission to listen for - * @param {any} expectedValue The value that should be emitted - * @returns {Promise} A Promise that fulfills if the value is emitted, and rejects if something else is emitted. - * The Promise will be indefinitely pending if no value is emitted. - */ - function assertEmitted(emitter, emitType, expectedValue) { - return new Promise((resolve, reject) => { - emitter.once(emitType, emittedValue => { - if (emittedValue === expectedValue) { - resolve(); - } else { - reject(new Error(`Expected ${expectedValue} to be emitted but ${emittedValue} was emitted instead.`)); - } - }); - }); - } + 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' }); + }, + }; + } - describe("naming test cases", () => { + ruleTester.run('function-style-rule', functionStyleRule, { + valid: [], + invalid: [{ code: 'var foo = bar;', errors: 1 }], + }); - it("should use the first argument as the name of the test suite", () => { - const assertion = assertEmitted(ruleTesterTestEmitter, "describe", "this-is-a-rule-name"); + 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/latest/extend/custom-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 }, + ], + }); - ruleTester.run("this-is-a-rule-name", require("./fixtures/no-var"), { - valid: [], - invalid: [] - }); + 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/latest/extend/custom-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 }, + ], + }); - return assertion; - }); + 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/latest/extend/custom-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 -- intentionally 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 }, + ], + }); - it("should use the test code as the name of the tests for valid code (string form)", () => { - const assertion = assertEmitted(ruleTesterTestEmitter, "it", "valid(code);"); + 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/latest/extend/custom-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 }, + ], + }); - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: [ - "valid(code);" - ], - invalid: [] - }); + 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/latest/extend/custom-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' }); + }, + }; + }, + }; - return assertion; - }); + ruleTester.run('rule-with-no-options', ruleWithEmptySchema, { + valid: [], + invalid: [{ code: 'var foo = bar;', errors: 1 }], + }); - it("should use the test code as the name of the tests for valid code (object form)", () => { - const assertion = assertEmitted(ruleTesterTestEmitter, "it", "valid(code);"); + assert.strictEqual( + processStub.callCount, + 0, + 'never calls `process.emitWarning()`', + ); + }); - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: [ - { - code: "valid(code);" - } - ], - invalid: [] - }); + 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 }], + }); - return assertion; - }); + assert.strictEqual( + processStub.callCount, + 0, + 'never calls `process.emitWarning()`', + ); + }); - it("should use the test code as the name of the tests for invalid code", () => { - const assertion = assertEmitted(ruleTesterTestEmitter, "it", "var x = invalid(code);"); + 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("foo", require("./fixtures/no-var"), { - valid: [], - invalid: [ - { - code: "var x = invalid(code);", - output: " x = invalid(code);", - errors: 1 - } - ] - }); + ruleTester.run('rule-with-schema', ruleWithSchema, { + valid: [], + invalid: [{ code: 'var foo = bar;', options: [true], errors: 1 }], + }); - return assertion; - }); + assert.strictEqual( + processStub.callCount, + 0, + 'never calls `process.emitWarning()`', + ); + }); - // https://github.com/eslint/eslint/issues/8142 - it("should use the empty string as the name of the test if the test case is an empty string", () => { - const assertion = assertEmitted(ruleTesterTestEmitter, "it", ""); + 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 }], + }); - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: [ - { - code: "" - } - ], - invalid: [] - }); + assert.strictEqual( + processStub.callCount, + 0, + 'never calls `process.emitWarning()`', + ); + }); - return assertion; - }); + 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 }], + }); - it('should use the "name" property if set to a non-empty string', () => { - const assertion = assertEmitted(ruleTesterTestEmitter, "it", "my test"); + 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 }], + }); - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: [], - invalid: [ - { - name: "my test", - code: "var x = invalid(code);", - output: " x = invalid(code);", - 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 + * @param {string} emitType The type of emission to listen for + * @param {any} expectedValue The value that should be emitted + * @returns {Promise} A Promise that fulfills if the value is emitted, and rejects if something else is emitted. + * The Promise will be indefinitely pending if no value is emitted. + */ + function assertEmitted(emitter, emitType, expectedValue) { + return new Promise((resolve, reject) => { + emitter.once(emitType, emittedValue => { + if (emittedValue === expectedValue) { + resolve(); + } else { + reject( + new Error( + `Expected ${expectedValue} to be emitted but ${emittedValue} was emitted instead.`, + ), + ); + } + }); + }); + } - return assertion; - }); + describe('naming test cases', () => { + it('should use the first argument as the name of the test suite', () => { + const assertion = assertEmitted( + ruleTesterTestEmitter, + 'describe', + 'this-is-a-rule-name', + ); - it('should use the "name" property if set to a non-empty string for valid cases too', () => { - const assertion = assertEmitted(ruleTesterTestEmitter, "it", "my test"); + ruleTester.run('this-is-a-rule-name', require('./fixtures/no-var'), { + valid: [], + invalid: [], + }); - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: [ - { - name: "my test", - code: "valid(code);" - } - ], - invalid: [] - }); + return assertion; + }); - return assertion; - }); + it('should use the test code as the name of the tests for valid code (string form)', () => { + const assertion = assertEmitted( + ruleTesterTestEmitter, + 'it', + 'valid(code);', + ); + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: ['valid(code);'], + invalid: [], + }); - it('should use the test code as the name if the "name" property is set to an empty string', () => { - const assertion = assertEmitted(ruleTesterTestEmitter, "it", "var x = invalid(code);"); + return assertion; + }); - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: [], - invalid: [ - { - name: "", - code: "var x = invalid(code);", - output: " x = invalid(code);", - errors: 1 - } - ] - }); + it('should use the test code as the name of the tests for valid code (object form)', () => { + const assertion = assertEmitted( + ruleTesterTestEmitter, + 'it', + 'valid(code);', + ); - return assertion; - }); + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: [ + { + code: 'valid(code);', + }, + ], + invalid: [], + }); - it('should throw if "name" property is not a string', () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: [{ code: "foo", name: 123 }], - invalid: [{ code: "foo" }] + return assertion; + }); + + it('should use the test code as the name of the tests for invalid code', () => { + const assertion = assertEmitted( + ruleTesterTestEmitter, + 'it', + 'var x = invalid(code);', + ); + + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: [], + invalid: [ + { + code: 'var x = invalid(code);', + output: ' x = invalid(code);', + errors: 1, + }, + ], + }); - }); - }, /Optional test case property 'name' must be a string/u); + return assertion; + }); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: ["foo"], - invalid: [{ code: "foo", name: 123 }] - }); - }, /Optional test case property 'name' must be a string/u); - }); + // https://github.com/eslint/eslint/issues/8142 + it('should use the empty string as the name of the test if the test case is an empty string', () => { + const assertion = assertEmitted(ruleTesterTestEmitter, 'it', ''); - it('should throw if "code" property is not a string', () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: [{ code: 123 }], - invalid: [{ code: "foo" }] + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: [ + { + code: '', + }, + ], + invalid: [], + }); - }); - }, /Test case must specify a string value for 'code'/u); + return assertion; + }); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: [123], - invalid: [{ code: "foo" }] + it('should use the "name" property if set to a non-empty string', () => { + const assertion = assertEmitted(ruleTesterTestEmitter, 'it', 'my test'); - }); - }, /Test case must specify a string value for 'code'/u); + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: [], + invalid: [ + { + name: 'my test', + code: 'var x = invalid(code);', + output: ' x = invalid(code);', + errors: 1, + }, + ], + }); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: ["foo"], - invalid: [{ code: 123 }] - }); - }, /Test case must specify a string value for 'code'/u); - }); + return assertion; + }); - it('should throw if "code" property is missing', () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: [{ }], - invalid: [{ code: "foo" }] + it('should use the "name" property if set to a non-empty string for valid cases too', () => { + const assertion = assertEmitted(ruleTesterTestEmitter, 'it', 'my test'); - }); - }, /Test case must specify a string value for 'code'/u); + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: [ + { + name: 'my test', + code: 'valid(code);', + }, + ], + invalid: [], + }); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: ["foo"], - invalid: [{ }] - }); - }, /Test case must specify a string value for 'code'/u); - }); - }); + return assertion; + }); + + it('should use the test code as the name if the "name" property is set to an empty string', () => { + const assertion = assertEmitted( + ruleTesterTestEmitter, + 'it', + 'var x = invalid(code);', + ); + + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: [], + invalid: [ + { + name: '', + code: 'var x = invalid(code);', + output: ' x = invalid(code);', + errors: 1, + }, + ], + }); - // https://github.com/eslint/eslint/issues/11615 - it("should fail the case if autofix made a syntax error.", () => { - assert.throw(() => { - ruleTester.run( - "foo", - { - meta: { - fixable: "code" - }, - create(context) { - return { - Identifier(node) { - context.report({ - node, - message: "make a syntax error", - fix(fixer) { - return fixer.replaceText(node, "one two"); - } - }); - } - }; - } - }, - { - valid: ["one()"], - invalid: [] - } - ); - }, /A fatal parsing error occurred in autofix.\nError: .+\nAutofix output:\n.+/u); + return assertion; }); - describe("sanitize test cases", () => { - let originalRuleTesterIt; - let spyRuleTesterIt; - - before(() => { - originalRuleTesterIt = RuleTester.it; - spyRuleTesterIt = sinon.spy(); - RuleTester.it = spyRuleTesterIt; - }); - after(() => { - RuleTester.it = originalRuleTesterIt; - }); - beforeEach(() => { - spyRuleTesterIt.resetHistory(); - ruleTester = new RuleTester(); + it('should throw if "name" property is not a string', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: [{ code: 'foo', name: 123 }], + invalid: [{ code: 'foo' }], }); - it("should present newline when using back-tick as new line", () => { - const code = ` - var foo = bar;`; + }, /Optional test case property 'name' must be a string/u); - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [], - invalid: [ - { - code, - errors: [/^Bad var/u] - } - ] - }); - sinon.assert.calledWith(spyRuleTesterIt, code); + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: ['foo'], + invalid: [{ code: 'foo', name: 123 }], }); - it("should present \\u0000 as a string", () => { - const code = "\u0000"; + }, /Optional test case property 'name' must be a string/u); + }); - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [], - invalid: [ - { - code, - errors: [/^Bad var/u] - } - ] - }); - sinon.assert.calledWith(spyRuleTesterIt, "\\u0000"); + it('should throw if "code" property is not a string', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: [{ code: 123 }], + invalid: [{ code: 'foo' }], }); - it("should present the pipe character correctly", () => { - const code = "var foo = bar || baz;"; + }, /Test case must specify a string value for 'code'/u); - ruleTester.run("no-var", require("./fixtures/no-var"), { - valid: [], - invalid: [ - { - code, - errors: [/^Bad var/u] - } - ] - }); - sinon.assert.calledWith(spyRuleTesterIt, code); + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: [123], + invalid: [{ code: 'foo' }], }); + }, /Test case must specify a string value for 'code'/u); + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: ['foo'], + invalid: [{ code: 123 }], + }); + }, /Test case must specify a string value for 'code'/u); }); - describe("SourceCode#getComments()", () => { - const useGetCommentsRule = { - create: context => ({ - Program(node) { - const sourceCode = context.getSourceCode(); - - sourceCode.getComments(node); - } - }) - }; - - it("should throw if called from a valid test case", () => { - assert.throws(() => { - ruleTester.run("use-get-comments", useGetCommentsRule, { - valid: [""], - invalid: [] - }); - }, /`SourceCode#getComments\(\)` is deprecated/u); + it('should throw if "code" property is missing', () => { + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: [{}], + invalid: [{ code: 'foo' }], }); + }, /Test case must specify a string value for 'code'/u); - it("should throw if called from an invalid test case", () => { - assert.throws(() => { - ruleTester.run("use-get-comments", useGetCommentsRule, { - valid: [], - invalid: [{ - code: "", - errors: [{}] - }] - }); - }, /`SourceCode#getComments\(\)` is deprecated/u); + assert.throws(() => { + ruleTester.run('foo', require('./fixtures/no-var'), { + valid: ['foo'], + invalid: [{}], }); + }, /Test case must specify a string value for 'code'/u); }); + }); - describe("Subclassing", () => { - - it("should allow subclasses to set the describe/it/itOnly statics and should correctly use those values", () => { - const assertionDescribe = assertEmitted(ruleTesterTestEmitter, "custom describe", "this-is-a-rule-name"); - const assertionIt = assertEmitted(ruleTesterTestEmitter, "custom it", "valid(code);"); - const assertionItOnly = assertEmitted(ruleTesterTestEmitter, "custom itOnly", "validOnly(code);"); - - /** - * Subclass for testing - */ - class RuleTesterSubclass extends RuleTester { } - RuleTesterSubclass.describe = function(text, method) { - ruleTesterTestEmitter.emit("custom describe", text, method); - return method.call(this); - }; - RuleTesterSubclass.it = function(text, method) { - ruleTesterTestEmitter.emit("custom it", text, method); - return method.call(this); - }; - RuleTesterSubclass.itOnly = function(text, method) { - ruleTesterTestEmitter.emit("custom itOnly", text, method); - return method.call(this); + // https://github.com/eslint/eslint/issues/11615 + it('should fail the case if autofix made a syntax error.', () => { + assert.throw(() => { + ruleTester.run( + 'foo', + { + meta: { + fixable: 'code', + }, + create(context) { + return { + Identifier(node) { + context.report({ + node, + message: 'make a syntax error', + fix(fixer) { + return fixer.replaceText(node, 'one two'); + }, + }); + }, }; + }, + }, + { + valid: ['one()'], + invalid: [], + }, + ); + }, /A fatal parsing error occurred in autofix.\nError: .+\nAutofix output:\n.+/u); + }); + + describe('sanitize test cases', () => { + let originalRuleTesterIt; + let spyRuleTesterIt; - const ruleTesterSubclass = new RuleTesterSubclass(); - - ruleTesterSubclass.run("this-is-a-rule-name", require("./fixtures/no-var"), { - valid: [ - "valid(code);", - { - code: "validOnly(code);", - only: true - } - ], - invalid: [] - }); + before(() => { + originalRuleTesterIt = RuleTester.it; + spyRuleTesterIt = sinon.spy(); + RuleTester.it = spyRuleTesterIt; + }); + after(() => { + RuleTester.it = originalRuleTesterIt; + }); + beforeEach(() => { + spyRuleTesterIt.resetHistory(); + ruleTester = new RuleTester(); + }); + it('should present newline when using back-tick as new line', () => { + const code = ` + var foo = bar;`; - return Promise.all([ - assertionDescribe, - assertionIt, - assertionItOnly - ]); - }); + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [], + invalid: [ + { + code, + errors: [/^Bad var/u], + }, + ], + }); + sinon.assert.calledWith(spyRuleTesterIt, code); + }); + it('should present \\u0000 as a string', () => { + const code = '\u0000'; + + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [], + invalid: [ + { + code, + errors: [/^Bad var/u], + }, + ], + }); + sinon.assert.calledWith(spyRuleTesterIt, '\\u0000'); + }); + it('should present the pipe character correctly', () => { + const code = 'var foo = bar || baz;'; + + ruleTester.run('no-var', require('./fixtures/no-var'), { + valid: [], + invalid: [ + { + code, + errors: [/^Bad var/u], + }, + ], + }); + sinon.assert.calledWith(spyRuleTesterIt, code); + }); + }); + + describe('SourceCode#getComments()', () => { + const useGetCommentsRule = { + create: context => ({ + Program(node) { + const sourceCode = context.getSourceCode(); + + sourceCode.getComments(node); + }, + }), + }; + + it('should throw if called from a valid test case', () => { + assert.throws(() => { + ruleTester.run('use-get-comments', useGetCommentsRule, { + valid: [''], + invalid: [], + }); + }, /`SourceCode#getComments\(\)` is deprecated/u); + }); + + it('should throw if called from an invalid test case', () => { + assert.throws(() => { + ruleTester.run('use-get-comments', useGetCommentsRule, { + valid: [], + invalid: [ + { + code: '', + errors: [{}], + }, + ], + }); + }, /`SourceCode#getComments\(\)` is deprecated/u); + }); + }); + + describe('Subclassing', () => { + it('should allow subclasses to set the describe/it/itOnly statics and should correctly use those values', () => { + const assertionDescribe = assertEmitted( + ruleTesterTestEmitter, + 'custom describe', + 'this-is-a-rule-name', + ); + const assertionIt = assertEmitted( + ruleTesterTestEmitter, + 'custom it', + 'valid(code);', + ); + const assertionItOnly = assertEmitted( + ruleTesterTestEmitter, + 'custom itOnly', + 'validOnly(code);', + ); + + /** + * Subclass for testing + */ + class RuleTesterSubclass extends RuleTester {} + RuleTesterSubclass.describe = function (text, method) { + ruleTesterTestEmitter.emit('custom describe', text, method); + return method.call(this); + }; + RuleTesterSubclass.it = function (text, method) { + ruleTesterTestEmitter.emit('custom it', text, method); + return method.call(this); + }; + RuleTesterSubclass.itOnly = function (text, method) { + ruleTesterTestEmitter.emit('custom itOnly', text, method); + return method.call(this); + }; + + const ruleTesterSubclass = new RuleTesterSubclass(); + + ruleTesterSubclass.run( + 'this-is-a-rule-name', + require('./fixtures/no-var'), + { + valid: [ + 'valid(code);', + { + code: 'validOnly(code);', + only: true, + }, + ], + invalid: [], + }, + ); + return Promise.all([assertionDescribe, assertionIt, assertionItOnly]); }); - + }); }); From d426fe0645fc5d15fade3135f89363da8aaf17b2 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Tue, 7 May 2024 06:55:52 -0500 Subject: [PATCH 3/8] unpretty --- .../tests/eslint-base/eslint-base.test.js | 6017 ++++++++--------- 1 file changed, 2740 insertions(+), 3277 deletions(-) diff --git a/packages/rule-tester/tests/eslint-base/eslint-base.test.js b/packages/rule-tester/tests/eslint-base/eslint-base.test.js index 8dae8336e345..850f044e5ed3 100644 --- a/packages/rule-tester/tests/eslint-base/eslint-base.test.js +++ b/packages/rule-tester/tests/eslint-base/eslint-base.test.js @@ -14,25 +14,25 @@ */ /* eslint-disable */ -'use strict'; +"use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ -const sinon = require('sinon'), - EventEmitter = require('events'), - { RuleTester } = require('../../dist/RuleTester'), - assert = require('chai').assert, - nodeAssert = require('assert'), - espree = require('espree'); +const sinon = require("sinon"), + EventEmitter = require("events"), + { RuleTester } = require("../../dist/RuleTester"), + assert = require("chai").assert, + nodeAssert = require("assert"), + espree = require("espree"); const NODE_ASSERT_STRICT_EQUAL_OPERATOR = (() => { - try { - nodeAssert.strictEqual(1, 2); - } catch (err) { - return err.operator; - } - throw new Error('unexpected successful assertion'); + try { + nodeAssert.strictEqual(1, 2); + } catch (err) { + return err.operator; + } + throw new Error("unexpected successful assertion"); })(); /** @@ -40,7 +40,8 @@ const NODE_ASSERT_STRICT_EQUAL_OPERATOR = (() => { * @returns {void} */ function noop() { - // do nothing. + + // do nothing. } //------------------------------------------------------------------------------ @@ -67,3425 +68,2887 @@ const ruleTesterTestEmitter = new EventEmitter(); // Tests //------------------------------------------------------------------------------ -describe('RuleTester', () => { - // Stub `describe()` and `it()` while this test suite. - before(() => { - RuleTester.describe = function (text, method) { - ruleTesterTestEmitter.emit('describe', text, method); - return method.call(this); - }; - RuleTester.it = function (text, method) { - ruleTesterTestEmitter.emit('it', text, method); - return method.call(this); - }; - }); - after(() => { - RuleTester.describe = null; - RuleTester.it = null; - }); - - let ruleTester; - - /** - * A helper function to verify Node.js core error messages. - * @param {string} actual The actual input - * @param {string} expected The expected input - * @returns {Function} Error callback to verify that the message is correct - * for the actual and expected input. - */ - function assertErrorMatches(actual, expected) { - const err = new nodeAssert.AssertionError({ - actual, - expected, - operator: NODE_ASSERT_STRICT_EQUAL_OPERATOR, - }); - - return err.message; - } - - beforeEach(() => { - RuleTester.resetDefaultConfig(); - ruleTester = new RuleTester(); - }); - - describe('only', () => { - describe('`itOnly` accessor', () => { - describe('when `itOnly` is set', () => { - before(() => { - RuleTester.itOnly = sinon.spy(); - }); - after(() => { - RuleTester.itOnly = void 0; - }); - beforeEach(() => { - RuleTester.itOnly.resetHistory(); - ruleTester = new RuleTester(); - }); +describe("RuleTester", () => { - it('is called by exclusive tests', () => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [ - { - code: 'const notVar = 42;', - only: true, - }, - ], - invalid: [], - }); + // Stub `describe()` and `it()` while this test suite. + before(() => { + RuleTester.describe = function(text, method) { + ruleTesterTestEmitter.emit("describe", text, method); + return method.call(this); + }; + RuleTester.it = function(text, method) { + ruleTesterTestEmitter.emit("it", text, method); + return method.call(this); + }; + }); + after(() => { + RuleTester.describe = null; + RuleTester.it = null; + }); - sinon.assert.calledWith(RuleTester.itOnly, 'const notVar = 42;'); - }); - }); + let ruleTester; - describe('when `it` is set and has an `only()` method', () => { - before(() => { - RuleTester.it.only = () => {}; - sinon.spy(RuleTester.it, 'only'); - }); - after(() => { - RuleTester.it.only = void 0; - }); - beforeEach(() => { - RuleTester.it.only.resetHistory(); - ruleTester = new RuleTester(); + /** + * A helper function to verify Node.js core error messages. + * @param {string} actual The actual input + * @param {string} expected The expected input + * @returns {Function} Error callback to verify that the message is correct + * for the actual and expected input. + */ + function assertErrorMatches(actual, expected) { + const err = new nodeAssert.AssertionError({ + actual, + expected, + operator: NODE_ASSERT_STRICT_EQUAL_OPERATOR }); - it('is called by tests with `only` set', () => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [ - { - code: 'const notVar = 42;', - only: true, - }, - ], - invalid: [], - }); - - sinon.assert.calledWith(RuleTester.it.only, 'const notVar = 42;'); - }); - }); + return err.message; + } - describe('when global `it` is a function that has an `only()` method', () => { - let originalGlobalItOnly; + beforeEach(() => { + RuleTester.resetDefaultConfig(); + ruleTester = new RuleTester(); + }); - before(() => { - /* - * We run tests with `--forbid-only`, so we have to override - * `it.only` to prevent the real one from being called. - */ - originalGlobalItOnly = it.only; - it.only = () => {}; - sinon.spy(it, 'only'); - }); - after(() => { - it.only = originalGlobalItOnly; - }); - beforeEach(() => { - it.only.resetHistory(); - ruleTester = new RuleTester(); - }); + describe("only", () => { + describe("`itOnly` accessor", () => { + describe("when `itOnly` is set", () => { + before(() => { + RuleTester.itOnly = sinon.spy(); + }); + after(() => { + RuleTester.itOnly = void 0; + }); + beforeEach(() => { + RuleTester.itOnly.resetHistory(); + ruleTester = new RuleTester(); + }); - it('is called by tests with `only` set', () => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [ - { - code: 'const notVar = 42;', - only: true, - }, - ], - invalid: [], - }); + it("is called by exclusive tests", () => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [{ + code: "const notVar = 42;", + only: true + }], + invalid: [] + }); - sinon.assert.calledWith(it.only, 'const notVar = 42;'); - }); - }); + sinon.assert.calledWith(RuleTester.itOnly, "const notVar = 42;"); + }); + }); - describe('when `describe` and `it` are overridden without `itOnly`', () => { - let originalGlobalItOnly; + describe("when `it` is set and has an `only()` method", () => { + before(() => { + RuleTester.it.only = () => {}; + sinon.spy(RuleTester.it, "only"); + }); + after(() => { + RuleTester.it.only = void 0; + }); + beforeEach(() => { + RuleTester.it.only.resetHistory(); + ruleTester = new RuleTester(); + }); - before(() => { - /* - * These tests override `describe` and `it` already, so we - * don't need to override them here. We do, however, need to - * remove `only` from the global `it` to prevent it from - * being used instead. - */ - originalGlobalItOnly = it.only; - it.only = void 0; - }); - after(() => { - it.only = originalGlobalItOnly; - }); - beforeEach(() => { - ruleTester = new RuleTester(); - }); + it("is called by tests with `only` set", () => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [{ + code: "const notVar = 42;", + only: true + }], + invalid: [] + }); - it('throws an error recommending overriding `itOnly`', () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [ - { - code: 'const notVar = 42;', - only: true, - }, - ], - invalid: [], + sinon.assert.calledWith(RuleTester.it.only, "const notVar = 42;"); + }); }); - }, 'Set `RuleTester.itOnly` to use `only` with a custom test framework.'); - }); - }); - describe('when global `it` is a function that does not have an `only()` method', () => { - let originalGlobalIt; - let originalRuleTesterDescribe; - let originalRuleTesterIt; + describe("when global `it` is a function that has an `only()` method", () => { + let originalGlobalItOnly; - before(() => { - originalGlobalIt = global.it; + before(() => { - // eslint-disable-next-line no-global-assign -- Temporarily override Mocha global - it = () => {}; + /* + * We run tests with `--forbid-only`, so we have to override + * `it.only` to prevent the real one from being called. + */ + originalGlobalItOnly = it.only; + it.only = () => {}; + sinon.spy(it, "only"); + }); + after(() => { + it.only = originalGlobalItOnly; + }); + beforeEach(() => { + it.only.resetHistory(); + ruleTester = new RuleTester(); + }); - /* - * These tests override `describe` and `it`, so we need to - * un-override them here so they won't interfere. - */ - originalRuleTesterDescribe = RuleTester.describe; - RuleTester.describe = void 0; - originalRuleTesterIt = RuleTester.it; - RuleTester.it = void 0; - }); - after(() => { - // eslint-disable-next-line no-global-assign -- Restore Mocha global - it = originalGlobalIt; - RuleTester.describe = originalRuleTesterDescribe; - RuleTester.it = originalRuleTesterIt; - }); - beforeEach(() => { - ruleTester = new RuleTester(); - }); + it("is called by tests with `only` set", () => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [{ + code: "const notVar = 42;", + only: true + }], + invalid: [] + }); - it('throws an error explaining that the current test framework does not support `only`', () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [ - { - code: 'const notVar = 42;', - only: true, - }, - ], - invalid: [], + sinon.assert.calledWith(it.only, "const notVar = 42;"); + }); }); - }, 'The current test framework does not support exclusive tests with `only`.'); - }); - }); - }); - describe('test cases', () => { - const ruleName = 'no-var'; - const rule = require('./fixtures/no-var'); + describe("when `describe` and `it` are overridden without `itOnly`", () => { + let originalGlobalItOnly; - let originalRuleTesterIt; - let spyRuleTesterIt; - let originalRuleTesterItOnly; - let spyRuleTesterItOnly; + before(() => { - before(() => { - originalRuleTesterIt = RuleTester.it; - spyRuleTesterIt = sinon.spy(); - RuleTester.it = spyRuleTesterIt; - originalRuleTesterItOnly = RuleTester.itOnly; - spyRuleTesterItOnly = sinon.spy(); - RuleTester.itOnly = spyRuleTesterItOnly; - }); - after(() => { - RuleTester.it = originalRuleTesterIt; - RuleTester.itOnly = originalRuleTesterItOnly; - }); - beforeEach(() => { - spyRuleTesterIt.resetHistory(); - spyRuleTesterItOnly.resetHistory(); - ruleTester = new RuleTester(); - }); + /* + * These tests override `describe` and `it` already, so we + * don't need to override them here. We do, however, need to + * remove `only` from the global `it` to prevent it from + * being used instead. + */ + originalGlobalItOnly = it.only; + it.only = void 0; + }); + after(() => { + it.only = originalGlobalItOnly; + }); + beforeEach(() => { + ruleTester = new RuleTester(); + }); - it("isn't called for normal tests", () => { - ruleTester.run(ruleName, rule, { - valid: ['const notVar = 42;'], - invalid: [], - }); - sinon.assert.calledWith(spyRuleTesterIt, 'const notVar = 42;'); - sinon.assert.notCalled(spyRuleTesterItOnly); - }); + it("throws an error recommending overriding `itOnly`", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [{ + code: "const notVar = 42;", + only: true + }], + invalid: [] + }); + }, "Set `RuleTester.itOnly` to use `only` with a custom test framework."); + }); + }); - it('calls it or itOnly for every test case', () => { - /* - * `RuleTester` doesn't implement test case exclusivity itself. - * Setting `only: true` just causes `RuleTester` to call - * whatever `only()` function is provided by the test framework - * instead of the regular `it()` function. - */ + describe("when global `it` is a function that does not have an `only()` method", () => { + let originalGlobalIt; + let originalRuleTesterDescribe; + let originalRuleTesterIt; + + before(() => { + originalGlobalIt = global.it; + + // eslint-disable-next-line no-global-assign -- Temporarily override Mocha global + it = () => {}; + + /* + * These tests override `describe` and `it`, so we need to + * un-override them here so they won't interfere. + */ + originalRuleTesterDescribe = RuleTester.describe; + RuleTester.describe = void 0; + originalRuleTesterIt = RuleTester.it; + RuleTester.it = void 0; + }); + after(() => { - ruleTester.run(ruleName, rule, { - valid: [ - 'const valid = 42;', - { - code: 'const onlyValid = 42;', - only: true, - }, - ], - invalid: [ - { - code: 'var invalid = 42;', - errors: [/^Bad var/u], - }, - { - code: 'var onlyInvalid = 42;', - errors: [/^Bad var/u], - only: true, - }, - ], + // eslint-disable-next-line no-global-assign -- Restore Mocha global + it = originalGlobalIt; + RuleTester.describe = originalRuleTesterDescribe; + RuleTester.it = originalRuleTesterIt; + }); + beforeEach(() => { + ruleTester = new RuleTester(); + }); + + it("throws an error explaining that the current test framework does not support `only`", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [{ + code: "const notVar = 42;", + only: true + }], + invalid: [] + }); + }, "The current test framework does not support exclusive tests with `only`."); + }); + }); }); - sinon.assert.calledWith(spyRuleTesterIt, 'const valid = 42;'); - sinon.assert.calledWith(spyRuleTesterItOnly, 'const onlyValid = 42;'); - sinon.assert.calledWith(spyRuleTesterIt, 'var invalid = 42;'); - sinon.assert.calledWith(spyRuleTesterItOnly, 'var onlyInvalid = 42;'); - }); - }); + describe("test cases", () => { + const ruleName = "no-var"; + const rule = require("./fixtures/no-var"); + + let originalRuleTesterIt; + let spyRuleTesterIt; + let originalRuleTesterItOnly; + let spyRuleTesterItOnly; + + before(() => { + originalRuleTesterIt = RuleTester.it; + spyRuleTesterIt = sinon.spy(); + RuleTester.it = spyRuleTesterIt; + originalRuleTesterItOnly = RuleTester.itOnly; + spyRuleTesterItOnly = sinon.spy(); + RuleTester.itOnly = spyRuleTesterItOnly; + }); + after(() => { + RuleTester.it = originalRuleTesterIt; + RuleTester.itOnly = originalRuleTesterItOnly; + }); + beforeEach(() => { + spyRuleTesterIt.resetHistory(); + spyRuleTesterItOnly.resetHistory(); + ruleTester = new RuleTester(); + }); + + it("isn't called for normal tests", () => { + ruleTester.run(ruleName, rule, { + valid: ["const notVar = 42;"], + invalid: [] + }); + sinon.assert.calledWith(spyRuleTesterIt, "const notVar = 42;"); + sinon.assert.notCalled(spyRuleTesterItOnly); + }); - describe('static helper wrapper', () => { - it('adds `only` to string test cases', () => { - const test = RuleTester.only('const valid = 42;'); + it("calls it or itOnly for every test case", () => { + + /* + * `RuleTester` doesn't implement test case exclusivity itself. + * Setting `only: true` just causes `RuleTester` to call + * whatever `only()` function is provided by the test framework + * instead of the regular `it()` function. + */ + + ruleTester.run(ruleName, rule, { + valid: [ + "const valid = 42;", + { + code: "const onlyValid = 42;", + only: true + } + ], + invalid: [ + { + code: "var invalid = 42;", + errors: [/^Bad var/u] + }, + { + code: "var onlyInvalid = 42;", + errors: [/^Bad var/u], + only: true + } + ] + }); - assert.deepStrictEqual(test, { - code: 'const valid = 42;', - only: true, + sinon.assert.calledWith(spyRuleTesterIt, "const valid = 42;"); + sinon.assert.calledWith(spyRuleTesterItOnly, "const onlyValid = 42;"); + sinon.assert.calledWith(spyRuleTesterIt, "var invalid = 42;"); + sinon.assert.calledWith(spyRuleTesterItOnly, "var onlyInvalid = 42;"); + }); }); - }); - it('adds `only` to object test cases', () => { - const test = RuleTester.only({ code: 'const valid = 42;' }); + describe("static helper wrapper", () => { + it("adds `only` to string test cases", () => { + const test = RuleTester.only("const valid = 42;"); + + assert.deepStrictEqual(test, { + code: "const valid = 42;", + only: true + }); + }); - assert.deepStrictEqual(test, { - code: 'const valid = 42;', - only: true, + it("adds `only` to object test cases", () => { + const test = RuleTester.only({ code: "const valid = 42;" }); + + assert.deepStrictEqual(test, { + code: "const valid = 42;", + only: true + }); + }); }); - }); }); - }); - - it('should not throw an error when everything passes', () => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['Eval(foo)'], - invalid: [ - { - code: 'eval(foo)', - errors: [{ message: 'eval sucks.', type: 'CallExpression' }], - }, - ], - }); - }); - - it('should throw an error when valid code is invalid', () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['eval(foo)'], - invalid: [ - { - code: 'eval(foo)', - errors: [{ message: 'eval sucks.', type: 'CallExpression' }], - }, - ], - }); - }, /Should have no errors but had 1/u); - }); - - it('should throw an error when valid code is invalid', () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: [{ code: 'eval(foo)' }], - invalid: [ - { - code: 'eval(foo)', - errors: [{ message: 'eval sucks.', type: 'CallExpression' }], - }, - ], - }); - }, /Should have no errors but had 1/u); - }); - - it('should throw an error if invalid code is valid', () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['Eval(foo)'], - invalid: [ - { - code: 'Eval(foo)', - errors: [{ message: 'eval sucks.', type: 'CallExpression' }], - }, - ], - }); - }, /Should have 1 error but had 0/u); - }); - - it('should throw an error when the error message is wrong', () => { - assert.throws( - () => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - // Only the invalid test matters here - valid: ['bar = baz;'], - invalid: [ - { - code: 'var foo = bar;', - errors: [{ message: 'Bad error message.' }], - }, - ], - }); - }, - assertErrorMatches('Bad var.', 'Bad error message.'), - ); - }); - - it('should throw an error when the error message regex does not match', () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [], - invalid: [ - { - code: 'var foo = bar;', - errors: [{ message: /Bad error message/u }], - }, - ], - }); - }, /Expected 'Bad var.' to match \/Bad error message\//u); - }); - - it('should throw an error when the error is not a supported type', () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - // Only the invalid test matters here - valid: ['bar = baz;'], - invalid: [{ code: 'var foo = bar;', errors: [42] }], - }); - }, /Error should be a string, object, or RegExp/u); - }); - - it('should throw an error when any of the errors is not a supported type', () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - // Only the invalid test matters here - valid: ['bar = baz;'], - invalid: [ - { - code: 'var foo = bar; var baz = quux', - errors: [{ type: 'VariableDeclaration' }, null], - }, - ], - }); - }, /Error should be a string, object, or RegExp/u); - }); - - it('should throw an error when the error is a string and it does not match error message', () => { - assert.throws( - () => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - // Only the invalid test matters here - valid: ['bar = baz;'], - invalid: [{ code: 'var foo = bar;', errors: ['Bad error message.'] }], - }); - }, - assertErrorMatches('Bad var.', 'Bad error message.'), - ); - }); - - it('should throw an error when the error is a string and it does not match error message', () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [], - invalid: [{ code: 'var foo = bar;', errors: [/Bad error message/u] }], - }); - }, /Expected 'Bad var.' to match \/Bad error message\//u); - }); - - it('should not throw an error when the error is a string and it matches error message', () => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - // Only the invalid test matters here - valid: ['bar = baz;'], - invalid: [ - { code: 'var foo = bar;', output: ' foo = bar;', errors: ['Bad var.'] }, - ], - }); - }); - - it('should not throw an error when the error is a regex and it matches error message', () => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [], - invalid: [ - { - code: 'var foo = bar;', - output: ' foo = bar;', - errors: [/^Bad var/u], - }, - ], - }); - }); - - it('should throw an error when the error is an object with an unknown property name', () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: ['bar = baz;'], - invalid: [ - { code: 'var foo = bar;', errors: [{ Message: 'Bad var.' }] }, - ], - }); - }, /Invalid error property name 'Message'/u); - }); - - it('should throw an error when any of the errors is an object with an unknown property name', () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: ['bar = baz;'], - invalid: [ - { - code: 'var foo = bar; var baz = quux', - errors: [ - { message: 'Bad var.', type: 'VariableDeclaration' }, - { message: 'Bad var.', typo: 'VariableDeclaration' }, - ], - }, - ], - }); - }, /Invalid error property name 'typo'/u); - }); - - it('should not throw an error when the error is a regex in an object and it matches error message', () => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [], - invalid: [ - { - code: 'var foo = bar;', - output: ' foo = bar;', - errors: [{ message: /^Bad var/u }], - }, - ], - }); - }); - - it("should throw an error when the expected output doesn't match", () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: ['bar = baz;'], - invalid: [ - { - code: 'var foo = bar;', - output: 'foo = bar', - errors: [{ message: 'Bad var.', type: 'VariableDeclaration' }], - }, - ], - }); - }, /Output is incorrect/u); - }); - - it('should use strict equality to compare output', () => { - const replaceProgramWith5Rule = { - meta: { - fixable: 'code', - }, - - create: context => ({ - Program(node) { - context.report({ - node, - message: 'bad', - fix: fixer => fixer.replaceText(node, '5'), - }); - }, - }), - }; - - // Should not throw. - ruleTester.run('foo', replaceProgramWith5Rule, { - valid: [], - invalid: [{ code: 'var foo = bar;', output: '5', errors: 1 }], - }); - - assert.throws(() => { - ruleTester.run('foo', replaceProgramWith5Rule, { - valid: [], - invalid: [{ code: 'var foo = bar;', output: 5, errors: 1 }], - }); - }, /Output is incorrect/u); - }); - - it("should throw an error when the expected output doesn't match and errors is just a number", () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: ['bar = baz;'], - invalid: [{ code: 'var foo = bar;', output: 'foo = bar', errors: 1 }], - }); - }, /Output is incorrect/u); - }); - - it('should not throw an error when the expected output is null and no errors produce output', () => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['bar = baz;'], - invalid: [ - { code: 'eval(x)', errors: 1, output: null }, - { code: 'eval(x); eval(y);', errors: 2, output: null }, - ], - }); - }); - - it('should throw an error when the expected output is null and problems produce output', () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: ['bar = baz;'], - invalid: [{ code: 'var foo = bar;', output: null, errors: 1 }], - }); - }, /Expected no autofixes to be suggested/u); - - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: ['bar = baz;'], - invalid: [ - { - code: 'var foo = bar; var qux = boop;', - output: null, - errors: 2, - }, - ], - }); - }, /Expected no autofixes to be suggested/u); - }); - - it('should throw an error when the expected output is null and only some problems produce output', () => { - assert.throws(() => { - ruleTester.run( - 'fixes-one-problem', - require('./fixtures/fixes-one-problem'), - { - valid: [], - invalid: [{ code: 'foo', output: null, errors: 2 }], - }, - ); - }, /Expected no autofixes to be suggested/u); - }); - - it("should throw an error when the expected output isn't specified and problems produce output", () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: ['bar = baz;'], - invalid: [{ code: 'var foo = bar;', errors: 1 }], - }); - }, "The rule fixed the code. Please add 'output' property."); - }); - - it('should throw an error if invalid code specifies wrong type', () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['Eval(foo)'], - invalid: [ - { - code: 'eval(foo)', - errors: [{ message: 'eval sucks.', type: 'CallExpression2' }], - }, - ], - }); - }, /Error type should be CallExpression2, found CallExpression/u); - }); - - it('should throw an error if invalid code specifies wrong line', () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['Eval(foo)'], - invalid: [ - { - code: 'eval(foo)', - errors: [ - { message: 'eval sucks.', type: 'CallExpression', line: 5 }, - ], - }, - ], - }); - }, /Error line should be 5/u); - }); - - it('should not skip line assertion if line is a falsy value', () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['Eval(foo)'], - invalid: [ - { - code: '\neval(foo)', - errors: [ - { message: 'eval sucks.', type: 'CallExpression', line: 0 }, - ], - }, - ], - }); - }, /Error line should be 0/u); - }); - - it('should throw an error if invalid code specifies wrong column', () => { - const wrongColumn = 10, - expectedErrorMessage = 'Error column should be 1'; - - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['Eval(foo)'], - invalid: [ - { - code: 'eval(foo)', - errors: [ - { - message: 'eval sucks.', - column: wrongColumn, - }, - ], - }, - ], - }); - }, expectedErrorMessage); - }); - - it('should throw error for empty error array', () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [], - }, - ], - }, - ); - }, /Invalid cases must have at least one error/u); - }); - - it('should throw error for errors : 0', () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: 0, - }, - ], - }, - ); - }, /Invalid cases must have 'error' value greater than 0/u); - }); - - it('should not skip column assertion if column is a falsy value', () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['Eval(foo)'], - invalid: [ - { - code: 'var foo; eval(foo)', - errors: [{ message: 'eval sucks.', column: 0 }], - }, - ], - }); - }, /Error column should be 0/u); - }); - - it('should throw an error if invalid code specifies wrong endLine', () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: ['bar = baz;'], - invalid: [ - { - code: 'var foo = bar;', - output: 'foo = bar', - errors: [ - { message: 'Bad var.', type: 'VariableDeclaration', endLine: 10 }, - ], - }, - ], - }); - }, 'Error endLine should be 10'); - }); - - it('should throw an error if invalid code specifies wrong endColumn', () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: ['bar = baz;'], - invalid: [ - { - code: 'var foo = bar;', - output: 'foo = bar', - errors: [ - { - message: 'Bad var.', - type: 'VariableDeclaration', - endColumn: 10, - }, - ], - }, - ], - }); - }, 'Error endColumn should be 10'); - }); - - it('should throw an error if invalid code has the wrong number of errors', () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['Eval(foo)'], - invalid: [ - { - code: 'eval(foo)', - errors: [ - { message: 'eval sucks.', type: 'CallExpression' }, - { message: 'eval sucks.', type: 'CallExpression' }, + + it("should not throw an error when everything passes", () => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "Eval(foo)" ], - }, - ], - }); - }, /Should have 2 errors but had 1/u); - }); - - it('should throw an error if invalid code does not have errors', () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['Eval(foo)'], - invalid: [{ code: 'eval(foo)' }], - }); - }, /Did not specify errors for an invalid test of no-eval/u); - }); - - it('should throw an error if invalid code has the wrong explicit number of errors', () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['Eval(foo)'], - invalid: [{ code: 'eval(foo)', errors: 2 }], - }); - }, /Should have 2 errors but had 1/u); - }); - - it("should throw an error if there's a parsing error in a valid test", () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ["1eval('foo')"], - invalid: [{ code: "eval('foo')", errors: [{}] }], - }); - }, /fatal parsing error/iu); - }); - - it("should throw an error if there's a parsing error in an invalid test", () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ["noeval('foo')"], - invalid: [{ code: "1eval('foo')", errors: [{}] }], - }); - }, /fatal parsing error/iu); - }); - - it("should throw an error if there's a parsing error in an invalid test and errors is just a number", () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ["noeval('foo')"], - invalid: [{ code: "1eval('foo')", errors: 1 }], - }); - }, /fatal parsing error/iu); - }); - - // https://github.com/eslint/eslint/issues/4779 - it("should throw an error if there's a parsing error and output doesn't match", () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: [], - invalid: [ - { - code: 'obvious parser error', - output: 'string that doesnt match', - errors: [{}], - }, - ], - }); - }, /fatal parsing error/iu); - }); - - it('should not throw an error if invalid code has at least an expected empty error object', () => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: ['Eval(foo)'], - invalid: [ - { - code: 'eval(foo)', - errors: [{}], - }, - ], - }); - }); - - it('should pass-through the globals config of valid tests to the to rule', () => { - ruleTester.run('no-test-global', require('./fixtures/no-test-global'), { - valid: [ - "var test = 'foo'", - { - code: "var test2 = 'bar'", - globals: { test: true }, - }, - ], - invalid: [{ code: 'bar', errors: 1 }], - }); - }); - - it('should pass-through the globals config of invalid tests to the to rule', () => { - ruleTester.run('no-test-global', require('./fixtures/no-test-global'), { - valid: ["var test = 'foo'"], - invalid: [ - { - code: "var test = 'foo'; var foo = 'bar'", - errors: 1, - }, - { - code: "var test = 'foo'", - globals: { foo: true }, - errors: [{ message: 'Global variable foo should not be used.' }], - }, - ], - }); - }); - - it('should pass-through the settings config to rules', () => { - ruleTester.run('no-test-settings', require('./fixtures/no-test-settings'), { - valid: [ - { - code: "var test = 'bar'", - settings: { test: 1 }, - }, - ], - invalid: [ - { - code: "var test = 'bar'", - settings: { 'no-test': 22 }, - errors: 1, - }, - ], - }); - }); - - it('should pass-through the filename to the rule', () => { - (function () { - ruleTester.run('', require('./fixtures/no-test-filename'), { - valid: [ - { - code: "var foo = 'bar'", - filename: 'somefile.js', - }, - ], - invalid: [ - { - code: "var foo = 'bar'", - errors: [{ message: 'Filename test was not defined.' }], - }, - ], - }); - })(); - }); - - it('should pass-through the options to the rule', () => { - ruleTester.run('no-invalid-args', require('./fixtures/no-invalid-args'), { - valid: [ - { - code: "var foo = 'bar'", - options: [false], - }, - ], - invalid: [ - { - code: "var foo = 'bar'", - options: [true], - errors: [{ message: 'Invalid args' }], - }, - ], - }); - }); - - it('should throw an error if the options are an object', () => { - assert.throws(() => { - ruleTester.run('no-invalid-args', require('./fixtures/no-invalid-args'), { - valid: [ - { - code: 'foo', - options: { ok: true }, - }, - ], - invalid: [], - }); - }, /options must be an array/u); - }); - - it('should throw an error if the options are a number', () => { - assert.throws(() => { - ruleTester.run('no-invalid-args', require('./fixtures/no-invalid-args'), { - valid: [ - { - code: 'foo', - options: 0, - }, - ], - invalid: [], - }); - }, /options must be an array/u); - }); - - it('should pass-through the parser to the rule', () => { - const spy = sinon.spy(ruleTester.linter, 'verify'); - - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: [ - { - code: 'Eval(foo)', - }, - ], - invalid: [ - { - code: 'eval(foo)', - parser: require.resolve('esprima'), - errors: [{ line: 1 }], - }, - ], - }); - assert.strictEqual(spy.args[1][1].parser, require.resolve('esprima')); - }); - - // skipping because it's not something our parser cares about - it.skip('should pass normalized ecmaVersion to the rule', () => { - const reportEcmaVersionRule = { - meta: { - messages: { - ecmaVersionMessage: - 'context.parserOptions.ecmaVersion is {{type}} {{ecmaVersion}}.', - }, - }, - create: context => ({ - Program(node) { - const { ecmaVersion } = context.parserOptions; - - context.report({ - node, - messageId: 'ecmaVersionMessage', - data: { type: typeof ecmaVersion, ecmaVersion }, - }); - }, - }), - }; - - const notEspree = require.resolve('./fixtures/empty-program-parser'); - - ruleTester.run('report-ecma-version', reportEcmaVersionRule, { - valid: [], - invalid: [ - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'undefined', ecmaVersion: 'undefined' }, - }, - ], - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'undefined', ecmaVersion: 'undefined' }, - }, - ], - parserOptions: {}, - }, - { - code: '
', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'undefined', ecmaVersion: 'undefined' }, - }, - ], - parserOptions: { ecmaFeatures: { jsx: true } }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'undefined', ecmaVersion: 'undefined' }, - }, - ], - parser: require.resolve('espree'), - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: '6' }, - }, - ], - parserOptions: { ecmaVersion: 6 }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: '6' }, - }, - ], - parserOptions: { ecmaVersion: 2015 }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'undefined', ecmaVersion: 'undefined' }, - }, - ], - env: { browser: true }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'undefined', ecmaVersion: 'undefined' }, - }, - ], - env: { es6: false }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: '6' }, - }, - ], - env: { es6: true }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: '8' }, - }, - ], - env: { es6: false, es2017: true }, - }, - { - code: 'let x', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: '6' }, - }, - ], - env: { es6: 'truthy' }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: '8' }, - }, - ], - env: { es2017: true }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: '11' }, - }, - ], - env: { es2020: true }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: '12' }, - }, - ], - env: { es2021: true }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { - type: 'number', - ecmaVersion: String(espree.latestEcmaVersion), - }, - }, - ], - parserOptions: { ecmaVersion: 'latest' }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { - type: 'number', - ecmaVersion: String(espree.latestEcmaVersion), - }, - }, - ], - parser: require.resolve('espree'), - parserOptions: { ecmaVersion: 'latest' }, - }, - { - code: '
', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { - type: 'number', - ecmaVersion: String(espree.latestEcmaVersion), - }, - }, - ], - parserOptions: { ecmaVersion: 'latest', ecmaFeatures: { jsx: true } }, - }, - { - code: "import 'foo'", - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { - type: 'number', - ecmaVersion: String(espree.latestEcmaVersion), - }, - }, - ], - parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { - type: 'number', - ecmaVersion: String(espree.latestEcmaVersion), - }, - }, - ], - parserOptions: { ecmaVersion: 'latest' }, - env: { es6: true }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { - type: 'number', - ecmaVersion: String(espree.latestEcmaVersion), - }, - }, - ], - parserOptions: { ecmaVersion: 'latest' }, - env: { es2020: true }, - }, - - // Non-Espree parsers normalize ecmaVersion if it's not "latest" - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'undefined', ecmaVersion: 'undefined' }, - }, - ], - parser: notEspree, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'undefined', ecmaVersion: 'undefined' }, - }, - ], - parser: notEspree, - parserOptions: {}, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: '5' }, - }, - ], - parser: notEspree, - parserOptions: { ecmaVersion: 5 }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: '6' }, - }, - ], - parser: notEspree, - parserOptions: { ecmaVersion: 6 }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: 6 }, - }, - ], - parser: notEspree, - parserOptions: { ecmaVersion: 2015 }, - }, - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'string', ecmaVersion: 'latest' }, - }, - ], - parser: notEspree, - parserOptions: { ecmaVersion: 'latest' }, - }, - ], - }); - - [{ parserOptions: { ecmaVersion: 6 } }, { env: { es6: true } }].forEach( - options => { - new RuleTester(options).run( - 'report-ecma-version', - reportEcmaVersionRule, - { - valid: [], invalid: [ - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: '6' }, - }, + { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression" }] } + ] + }); + }); + + it("should throw an error when valid code is invalid", () => { + + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "eval(foo)" ], - }, - { - code: '', - parserOptions: {}, - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'number', ecmaVersion: '6' }, - }, + invalid: [ + { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression" }] } + ] + }); + }, /Should have no errors but had 1/u); + }); + + it("should throw an error when valid code is invalid", () => { + + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + { code: "eval(foo)" } ], - }, - ], - }, - ); - }, - ); - - new RuleTester({ parser: notEspree }).run( - 'report-ecma-version', - reportEcmaVersionRule, - { - valid: [], - invalid: [ - { - code: '', - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'undefined', ecmaVersion: 'undefined' }, - }, - ], - }, - { - code: '', - parserOptions: { ecmaVersion: 'latest' }, - errors: [ - { - messageId: 'ecmaVersionMessage', - data: { type: 'string', ecmaVersion: 'latest' }, - }, - ], - }, - ], - }, - ); - }); - - it('should pass-through services from parseForESLint to the rule', () => { - const enhancedParserPath = require.resolve('./fixtures/enhanced-parser'); - const disallowHiRule = { - create: context => ({ - Literal(node) { - const disallowed = context.parserServices.test.getMessage(); // returns "Hi!" - - if (node.value === disallowed) { - context.report({ node, message: `Don't use '${disallowed}'` }); - } - }, - }), - }; - - ruleTester.run('no-hi', disallowHiRule, { - valid: [ - { - code: "'Hello!'", - parser: enhancedParserPath, - }, - ], - invalid: [ - { - code: "'Hi!'", - parser: enhancedParserPath, - errors: [{ message: "Don't use 'Hi!'" }], - }, - ], - }); - }); - - it('should prevent invalid options schemas', () => { - assert.throws(() => { - ruleTester.run( - 'no-invalid-schema', - require('./fixtures/no-invalid-schema'), - { - valid: [ - 'var answer = 6 * 7;', - { code: 'var answer = 6 * 7;', options: [] }, - ], - invalid: [ - { - code: 'var answer = 6 * 7;', - options: ['bar'], - errors: [{ message: 'Expected nothing.' }], - }, - ], - }, - ); - }, 'Schema for rule no-invalid-schema is invalid:,\titems: should be object\n\titems[0].enum: should NOT have fewer than 1 items\n\titems: should match some schema in anyOf'); - }); - - it('should prevent schema violations in options', () => { - assert.throws(() => { - ruleTester.run( - 'no-schema-violation', - require('./fixtures/no-schema-violation'), - { - valid: [ - 'var answer = 6 * 7;', - { code: 'var answer = 6 * 7;', options: ['foo'] }, - ], - invalid: [ - { - code: 'var answer = 6 * 7;', - options: ['bar'], - errors: [{ message: 'Expected foo.' }], - }, - ], - }, - ); - }, /Value "bar" should be equal to one of the allowed values./u); - }); - - it('should disallow invalid defaults in rules', () => { - const ruleWithInvalidDefaults = { - meta: { - schema: [ - { - oneOf: [ - { enum: ['foo'] }, - { - type: 'object', - properties: { - foo: { - enum: ['foo', 'bar'], - default: 'foo', - }, - }, - additionalProperties: false, - }, - ], - }, - ], - }, - create: () => ({}), - }; - - assert.throws(() => { - ruleTester.run('invalid-defaults', ruleWithInvalidDefaults, { - valid: [ - { - code: 'foo', - options: [{}], - }, - ], - invalid: [], - }); - }, /Schema for rule invalid-defaults is invalid: default is ignored for: data1\.foo/u); - }); - - it('throw an error when an unknown config option is included', () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: [{ code: 'Eval(foo)', foo: 'bar' }], - invalid: [], - }); - }, /ESLint configuration in rule-tester is invalid./u); - }); - - it('throw an error when an invalid config value is included', () => { - assert.throws(() => { - ruleTester.run('no-eval', require('./fixtures/no-eval'), { - valid: [{ code: 'Eval(foo)', env: ['es6'] }], - invalid: [], - }); - }, /Property "env" is the wrong type./u); - }); + invalid: [ + { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression" }] } + ] + }); + }, /Should have no errors but had 1/u); + }); + + it("should throw an error if invalid code is valid", () => { - it('should pass-through the tester config to the rule', () => { - ruleTester = new RuleTester({ - globals: { test: true }, + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "Eval(foo)" + ], + invalid: [ + { code: "Eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression" }] } + ] + }); + }, /Should have 1 error but had 0/u); }); - ruleTester.run('no-test-global', require('./fixtures/no-test-global'), { - valid: ["var test = 'foo'", 'var test2 = test'], - invalid: [{ code: 'bar', errors: 1, globals: { foo: true } }], + it("should throw an error when the error message is wrong", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + + // Only the invalid test matters here + valid: [ + "bar = baz;" + ], + invalid: [ + { code: "var foo = bar;", errors: [{ message: "Bad error message." }] } + ] + }); + }, assertErrorMatches("Bad var.", "Bad error message.")); }); - }); - it('should correctly set the globals configuration', () => { - const config = { globals: { test: true } }; + it("should throw an error when the error message regex does not match", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: [{ message: /Bad error message/u }] } + ] + }); + }, /Expected 'Bad var.' to match \/Bad error message\//u); + }); - RuleTester.setDefaultConfig(config); - assert( - RuleTester.getDefaultConfig().globals.test, - 'The default config object is incorrect', - ); - }); + it("should throw an error when the error is not a supported type", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { - it('should correctly reset the global configuration', () => { - const config = { globals: { test: true } }; + // Only the invalid test matters here + valid: [ + "bar = baz;" + ], + invalid: [ + { code: "var foo = bar;", errors: [42] } + ] + }); + }, /Error should be a string, object, or RegExp/u); + }); - RuleTester.setDefaultConfig(config); - RuleTester.resetDefaultConfig(); - assert.deepStrictEqual( - RuleTester.getDefaultConfig(), - { parser: require.resolve('@typescript-eslint/parser'), rules: {} }, - 'The default configuration has not reset correctly', - ); - }); + it("should throw an error when any of the errors is not a supported type", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { - it('should enforce the global configuration to be an object', () => { - /** - * Set the default config for the rules tester - * @param {Object} config configuration object - * @returns {Function} Function to be executed - * @private - */ - function setConfig(config) { - return function () { - RuleTester.setDefaultConfig(config); - }; - } - assert.throw(setConfig()); - assert.throw(setConfig(1)); - assert.throw(setConfig(3.14)); - assert.throw(setConfig('foo')); - assert.throw(setConfig(null)); - assert.throw(setConfig(true)); - }); - - it('should pass-through the globals config to the tester then to the to rule', () => { - const config = { globals: { test: true } }; - - RuleTester.setDefaultConfig(config); - ruleTester = new RuleTester(); - - ruleTester.run('no-test-global', require('./fixtures/no-test-global'), { - valid: ["var test = 'foo'", 'var test2 = test'], - invalid: [{ code: 'bar', errors: 1, globals: { foo: true } }], - }); - }); - - it('should throw an error if AST was modified', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast'), { - valid: ['var foo = 0;'], - invalid: [], - }); - }, 'Rule should not modify AST.'); - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast'), { - valid: [], - invalid: [{ code: 'var bar = 0;', errors: ['error'] }], - }); - }, 'Rule should not modify AST.'); - }); - - it('should throw an error if AST was modified (at Program)', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast-at-first'), { - valid: ['var foo = 0;'], - invalid: [], - }); - }, 'Rule should not modify AST.'); - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast-at-first'), { - valid: [], - invalid: [{ code: 'var bar = 0;', errors: ['error'] }], - }); - }, 'Rule should not modify AST.'); - }); - - it('should throw an error if AST was modified (at Program:exit)', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), { - valid: ['var foo = 0;'], - invalid: [], - }); - }, 'Rule should not modify AST.'); - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), { - valid: [], - invalid: [{ code: 'var bar = 0;', errors: ['error'] }], - }); - }, 'Rule should not modify AST.'); - }); - - it('should throw an error if rule uses start and end properties on nodes, tokens or comments', () => { - const usesStartEndRule = { - create(context) { - return { - CallExpression(node) { - noop(node.arguments[1].start); - }, - "BinaryExpression[operator='+']"(node) { - noop(node.end); - }, - "UnaryExpression[operator='-']"(node) { - noop(context.sourceCode.getFirstToken(node).start); - }, - ConditionalExpression(node) { - noop(context.sourceCode.getFirstToken(node).end); - }, - BlockStatement(node) { - noop(context.sourceCode.getCommentsInside(node)[0].start); - }, - ObjectExpression(node) { - noop(context.sourceCode.getCommentsInside(node)[0].end); - }, - Decorator(node) { - noop(node.start); - }, - }; - }, - }; + // Only the invalid test matters here + valid: [ + "bar = baz;" + ], + invalid: [ + { code: "var foo = bar; var baz = quux", errors: [{ type: "VariableDeclaration" }, null] } + ] + }); + }, /Error should be a string, object, or RegExp/u); + }); - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: ['foo(a, b)'], - invalid: [], - }); - }, 'Use node.range[0] instead of node.start'); - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: [], - invalid: [{ code: 'var a = b * (c + d) / e;', errors: 1 }], - }); - }, 'Use node.range[1] instead of node.end'); - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: [], - invalid: [{ code: 'var a = -b * c;', errors: 1 }], - }); - }, 'Use token.range[0] instead of token.start'); - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: ['var a = b ? c : d;'], - invalid: [], - }); - }, 'Use token.range[1] instead of token.end'); - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: ['function f() { /* comment */ }'], - invalid: [], - }); - }, 'Use token.range[0] instead of token.start'); - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: [], - invalid: [{ code: 'var x = //\n {\n //comment\n //\n}', errors: 1 }], - }); - }, 'Use token.range[1] instead of token.end'); + it("should throw an error when the error is a string and it does not match error message", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { - const enhancedParserPath = require.resolve('./fixtures/enhanced-parser'); + // Only the invalid test matters here + valid: [ + "bar = baz;" + ], + invalid: [ + { code: "var foo = bar;", errors: ["Bad error message."] } + ] + }); + }, assertErrorMatches("Bad var.", "Bad error message.")); + }); - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: [{ code: 'foo(a, b)', parser: enhancedParserPath }], - invalid: [], - }); - }, 'Use node.range[0] instead of node.start'); - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: [], - invalid: [ - { - code: 'var a = b * (c + d) / e;', - parser: enhancedParserPath, - errors: 1, - }, - ], - }); - }, 'Use node.range[1] instead of node.end'); - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: [], - invalid: [ - { code: 'var a = -b * c;', parser: enhancedParserPath, errors: 1 }, - ], - }); - }, 'Use token.range[0] instead of token.start'); - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: [{ code: 'var a = b ? c : d;', parser: enhancedParserPath }], - invalid: [], - }); - }, 'Use token.range[1] instead of token.end'); - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: [ - { - code: 'function f() { /* comment */ }', - parser: enhancedParserPath, - }, - ], - invalid: [], - }); - }, 'Use token.range[0] instead of token.start'); - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: [], - invalid: [ - { - code: 'var x = //\n {\n //comment\n //\n}', - parser: enhancedParserPath, - errors: 1, - }, - ], - }); - }, 'Use token.range[1] instead of token.end'); - - assert.throws(() => { - ruleTester.run('uses-start-end', usesStartEndRule, { - valid: [ - { - code: '@foo class A {}', - parser: require.resolve('./fixtures/enhanced-parser2'), - }, - ], - invalid: [], - }); - }, 'Use node.range[0] instead of node.start'); - }); - - it('should throw an error if no test scenarios given', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast-at-last')); - }, 'Test Scenarios for rule foo : Could not find test scenario object'); - }); - - it('should throw an error if no acceptable test scenario object is given', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), []); - }, 'Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios\nCould not find any invalid test scenarios'); - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), ''); - }, 'Test Scenarios for rule foo : Could not find test scenario object'); - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), 2); - }, 'Test Scenarios for rule foo : Could not find test scenario object'); - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), {}); - }, 'Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios\nCould not find any invalid test scenarios'); - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), { - valid: [], - }); - }, 'Test Scenarios for rule foo is invalid:\nCould not find any invalid test scenarios'); - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/modify-ast-at-last'), { - invalid: [], - }); - }, 'Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios'); - }); - - // Nominal message/messageId use cases - it('should assert match if message provided in both test and result.', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/messageId').withMessageOnly, { - valid: [], - invalid: [{ code: 'foo', errors: [{ message: 'something' }] }], - }); - }, /Avoid using variables named/u); - - ruleTester.run('foo', require('./fixtures/messageId').withMessageOnly, { - valid: [], - invalid: [ - { - code: 'foo', - errors: [{ message: "Avoid using variables named 'foo'." }], - }, - ], - }); - }); - - it('should assert match between messageId if provided in both test and result.', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/messageId').withMetaWithData, { - valid: [], - invalid: [{ code: 'foo', errors: [{ messageId: 'unused' }] }], - }); - }, "messageId 'avoidFoo' does not match expected messageId 'unused'."); - - ruleTester.run('foo', require('./fixtures/messageId').withMetaWithData, { - valid: [], - invalid: [{ code: 'foo', errors: [{ messageId: 'avoidFoo' }] }], - }); - }); - it('should assert match between resulting message output if messageId and data provided in both test and result', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/messageId').withMetaWithData, { - valid: [], - invalid: [ - { - code: 'foo', - errors: [{ messageId: 'avoidFoo', data: { name: 'notFoo' } }], - }, - ], - }); - }, 'Hydrated message "Avoid using variables named \'notFoo\'." does not match "Avoid using variables named \'foo\'."'); - }); - - // messageId/message misconfiguration cases - it('should throw if user tests for both message and messageId', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/messageId').withMetaWithData, { - valid: [], - invalid: [ - { - code: 'foo', - errors: [{ message: 'something', messageId: 'avoidFoo' }], - }, - ], - }); - }, "Error should not specify both 'message' and a 'messageId'."); - }); - it("should throw if user tests for messageId but the rule doesn't use the messageId meta syntax.", () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/messageId').withMessageOnly, { - valid: [], - invalid: [{ code: 'foo', errors: [{ messageId: 'avoidFoo' }] }], - }); - }, "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'"); - }); - it("should throw if user tests for messageId not listed in the rule's meta syntax.", () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/messageId').withMetaWithData, { - valid: [], - invalid: [{ code: 'foo', errors: [{ messageId: 'useFoo' }] }], - }); - }, /Invalid messageId 'useFoo'/u); - }); - it('should throw if data provided without messageId.', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/messageId').withMetaWithData, { - valid: [], - invalid: [{ code: 'foo', errors: [{ data: 'something' }] }], - }); - }, "Error must specify 'messageId' if 'data' is used."); - }); - - describe('suggestions', () => { - it('should pass with valid suggestions (tested using desc)', () => { - ruleTester.run( - 'suggestions-basic', - require('./fixtures/suggestions').basic, - { - valid: ['var boo;'], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - desc: "Rename identifier 'foo' to 'bar'", - output: 'var bar;', - }, - ], - }, - ], - }, - ], - }, - ); - }); - - it('should pass with suggestions on multiple lines', () => { - ruleTester.run( - 'suggestions-basic', - require('./fixtures/suggestions').basic, - { - valid: [], - invalid: [ - { - code: 'function foo() {\n var foo = 1;\n}', - errors: [ - { - suggestions: [ - { - desc: "Rename identifier 'foo' to 'bar'", - output: 'function bar() {\n var foo = 1;\n}', - }, - ], - }, - { - suggestions: [ - { - desc: "Rename identifier 'foo' to 'bar'", - output: 'function foo() {\n var bar = 1;\n}', - }, - ], - }, - ], - }, - ], - }, - ); - }); - - it('should pass with valid suggestions (tested using messageIds)', () => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - messageId: 'renameFoo', - output: 'var bar;', - }, - { - messageId: 'renameFoo', - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }); - - it('should pass with valid suggestions (one tested using messageIds, the other using desc)', () => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - messageId: 'renameFoo', - output: 'var bar;', - }, - { - desc: "Rename identifier 'foo' to 'baz'", - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }); - - it('should pass with valid suggestions (tested using both desc and messageIds for the same suggestion)', () => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - desc: "Rename identifier 'foo' to 'bar'", - messageId: 'renameFoo', - output: 'var bar;', - }, - { - desc: "Rename identifier 'foo' to 'baz'", - messageId: 'renameFoo', - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }); - - it('should pass with valid suggestions (tested using only desc on a rule that utilizes meta.messages)', () => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - desc: "Rename identifier 'foo' to 'bar'", - output: 'var bar;', - }, - { - desc: "Rename identifier 'foo' to 'baz'", - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }); - - it('should pass with valid suggestions (tested using messageIds and data)', () => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - messageId: 'renameFoo', - data: { newName: 'bar' }, - output: 'var bar;', - }, - { - messageId: 'renameFoo', - data: { newName: 'baz' }, - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }); - - it('should pass when tested using empty suggestion test objects if the array length is correct', () => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [{}, {}], - }, - ], - }, - ], - }, - ); - }); - - it('should support explicitly expecting no suggestions', () => { - [void 0, null, false, []].forEach(suggestions => { - ruleTester.run('suggestions-basic', require('./fixtures/no-eval'), { - valid: [], - invalid: [ - { - code: "eval('var foo');", - errors: [ - { - suggestions, - }, - ], - }, - ], + it("should throw an error when the error is a string and it does not match error message", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + + valid: [ + ], + invalid: [ + { code: "var foo = bar;", errors: [/Bad error message/u] } + ] + }); + }, /Expected 'Bad var.' to match \/Bad error message\//u); + }); + + it("should not throw an error when the error is a string and it matches error message", () => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + + // Only the invalid test matters here + valid: [ + "bar = baz;" + ], + invalid: [ + { code: "var foo = bar;", output: " foo = bar;", errors: ["Bad var."] } + ] + }); + }); + + it("should not throw an error when the error is a regex and it matches error message", () => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [], + invalid: [ + { code: "var foo = bar;", output: " foo = bar;", errors: [/^Bad var/u] } + ] }); - }); }); - it('should fail when expecting no suggestions and there are suggestions', () => { - [void 0, null, false, []].forEach(suggestions => { + it("should throw an error when the error is an object with an unknown property name", () => { assert.throws(() => { - ruleTester.run( - 'suggestions-basic', - require('./fixtures/suggestions').basic, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions, - }, - ], - }, - ], - }, - ); - }, 'Error should have no suggestions on error with message: "Avoid using identifiers named \'foo\'."'); - }); + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [ + "bar = baz;" + ], + invalid: [ + { code: "var foo = bar;", errors: [{ Message: "Bad var." }] } + ] + }); + }, /Invalid error property name 'Message'/u); }); - it("should fail when testing for suggestions that don't exist", () => { - assert.throws(() => { - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ + it("should throw an error when any of the errors is an object with an unknown property name", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [ + "bar = baz;" + ], + invalid: [ { - messageId: 'this-does-not-exist', - }, - ], - }, - ], - }, - ], - }); - }, 'Error should have an array of suggestions. Instead received "undefined" on error with message: "Bad var."'); + code: "var foo = bar; var baz = quux", + errors: [ + { message: "Bad var.", type: "VariableDeclaration" }, + { message: "Bad var.", typo: "VariableDeclaration" } + ] + } + ] + }); + }, /Invalid error property name 'typo'/u); }); - it('should fail when there are a different number of suggestions', () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-basic', - require('./fixtures/suggestions').basic, - { + it("should not throw an error when the error is a regex in an object and it matches error message", () => { + ruleTester.run("no-var", require("./fixtures/no-var"), { valid: [], invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - desc: "Rename identifier 'foo' to 'bar'", - output: 'var bar;', - }, - { - desc: "Rename identifier 'foo' to 'baz'", - output: 'var baz;', - }, - ], - }, + { code: "var foo = bar;", output: " foo = bar;", errors: [{ message: /^Bad var/u }] } + ] + }); + }); + + it("should throw an error when the expected output doesn't match", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [ + "bar = baz;" ], - }, - ], - }, - ); - }, 'Error should have 2 suggestions. Instead found 1 suggestions'); + invalid: [ + { code: "var foo = bar;", output: "foo = bar", errors: [{ message: "Bad var.", type: "VariableDeclaration" }] } + ] + }); + }, /Output is incorrect/u); }); - it('should throw if suggestion fix made a syntax error.', () => { - assert.throw(() => { - ruleTester.run( - 'foo', - { - meta: { hasSuggestions: true }, - create(context) { - return { - Identifier(node) { - context.report({ - node, - message: 'make a syntax error', - suggest: [ - { - desc: 'make a syntax error', - fix(fixer) { - return fixer.replaceText(node, 'one two'); - }, - }, - ], - }); - }, - }; + it("should use strict equality to compare output", () => { + const replaceProgramWith5Rule = { + meta: { + fixable: "code" }, - }, - { - valid: [''], + + create: context => ({ + Program(node) { + context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); + } + }) + }; + + // Should not throw. + ruleTester.run("foo", replaceProgramWith5Rule, { + valid: [], invalid: [ - { - code: 'one()', - errors: [ - { - message: 'make a syntax error', - suggestions: [ - { - desc: 'make a syntax error', - output: 'one two()', - }, - ], - }, + { code: "var foo = bar;", output: "5", errors: 1 } + ] + }); + + assert.throws(() => { + ruleTester.run("foo", replaceProgramWith5Rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", output: 5, errors: 1 } + ] + }); + }, /Output is incorrect/u); + }); + + it("should throw an error when the expected output doesn't match and errors is just a number", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [ + "bar = baz;" ], - }, - ], - }, - ); - }, /A fatal parsing error occurred in suggestion fix\.\nError: .+\nSuggestion output:\n.+/u); + invalid: [ + { code: "var foo = bar;", output: "foo = bar", errors: 1 } + ] + }); + }, /Output is incorrect/u); }); - it("should throw if the suggestion description doesn't match", () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-basic', - require('./fixtures/suggestions').basic, - { - valid: [], + it("should not throw an error when the expected output is null and no errors produce output", () => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "bar = baz;" + ], invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - desc: 'not right', - output: 'var baz;', - }, - ], - }, + { code: "eval(x)", errors: 1, output: null }, + { code: "eval(x); eval(y);", errors: 2, output: null } + ] + }); + }); + + it("should throw an error when the expected output is null and problems produce output", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [ + "bar = baz;" + ], + invalid: [ + { code: "var foo = bar;", output: null, errors: 1 } + ] + }); + }, /Expected no autofixes to be suggested/u); + + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [ + "bar = baz;" + ], + invalid: [ + { + code: "var foo = bar; var qux = boop;", + output: null, + errors: 2 + } + ] + }); + }, /Expected no autofixes to be suggested/u); + }); + + it("should throw an error when the expected output is null and only some problems produce output", () => { + assert.throws(() => { + ruleTester.run("fixes-one-problem", require("./fixtures/fixes-one-problem"), { + valid: [], + invalid: [ + { code: "foo", output: null, errors: 2 } + ] + }); + }, /Expected no autofixes to be suggested/u); + }); + + it("should throw an error when the expected output isn't specified and problems produce output", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [ + "bar = baz;" + ], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "The rule fixed the code. Please add 'output' property."); + }); + + it("should throw an error if invalid code specifies wrong type", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "Eval(foo)" + ], + invalid: [ + { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression2" }] } + ] + }); + }, /Error type should be CallExpression2, found CallExpression/u); + }); + + it("should throw an error if invalid code specifies wrong line", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "Eval(foo)" + ], + invalid: [ + { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression", line: 5 }] } + ] + }); + }, /Error line should be 5/u); + }); + + it("should not skip line assertion if line is a falsy value", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "Eval(foo)" + ], + invalid: [ + { code: "\neval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression", line: 0 }] } + ] + }); + }, /Error line should be 0/u); + }); + + it("should throw an error if invalid code specifies wrong column", () => { + const wrongColumn = 10, + expectedErrorMessage = "Error column should be 1"; + + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: ["Eval(foo)"], + invalid: [{ + code: "eval(foo)", + errors: [{ + message: "eval sucks.", + column: wrongColumn + }] + }] + }); + }, expectedErrorMessage); + }); + + it("should throw error for empty error array", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [] + }] + }); + }, /Invalid cases must have at least one error/u); + }); + + it("should throw error for errors : 0", () => { + assert.throws(() => { + ruleTester.run( + "suggestions-messageIds", + require("./fixtures/suggestions") + .withMessageIds, + { + valid: [], + invalid: [ + { + code: "var foo;", + errors: 0 + } + ] + } + ); + }, /Invalid cases must have 'error' value greater than 0/u); + }); + + it("should not skip column assertion if column is a falsy value", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: ["Eval(foo)"], + invalid: [{ + code: "var foo; eval(foo)", + errors: [{ message: "eval sucks.", column: 0 }] + }] + }); + }, /Error column should be 0/u); + }); + + it("should throw an error if invalid code specifies wrong endLine", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [ + "bar = baz;" + ], + invalid: [ + { code: "var foo = bar;", output: "foo = bar", errors: [{ message: "Bad var.", type: "VariableDeclaration", endLine: 10 }] } + ] + }); + }, "Error endLine should be 10"); + }); + + it("should throw an error if invalid code specifies wrong endColumn", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [ + "bar = baz;" + ], + invalid: [ + { code: "var foo = bar;", output: "foo = bar", errors: [{ message: "Bad var.", type: "VariableDeclaration", endColumn: 10 }] } + ] + }); + }, "Error endColumn should be 10"); + }); + + it("should throw an error if invalid code has the wrong number of errors", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "Eval(foo)" + ], + invalid: [ + { + code: "eval(foo)", + errors: [ + { message: "eval sucks.", type: "CallExpression" }, + { message: "eval sucks.", type: "CallExpression" } + ] + } + ] + }); + }, /Should have 2 errors but had 1/u); + }); + + it("should throw an error if invalid code does not have errors", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "Eval(foo)" + ], + invalid: [ + { code: "eval(foo)" } + ] + }); + }, /Did not specify errors for an invalid test of no-eval/u); + }); + + it("should throw an error if invalid code has the wrong explicit number of errors", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "Eval(foo)" + ], + invalid: [ + { code: "eval(foo)", errors: 2 } + ] + }); + }, /Should have 2 errors but had 1/u); + }); + + it("should throw an error if there's a parsing error in a valid test", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "1eval('foo')" + ], + invalid: [ + { code: "eval('foo')", errors: [{}] } + ] + }); + }, /fatal parsing error/iu); + }); + + it("should throw an error if there's a parsing error in an invalid test", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "noeval('foo')" + ], + invalid: [ + { code: "1eval('foo')", errors: [{}] } + ] + }); + }, /fatal parsing error/iu); + }); + + it("should throw an error if there's a parsing error in an invalid test and errors is just a number", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + "noeval('foo')" ], - }, + invalid: [ + { code: "1eval('foo')", errors: 1 } + ] + }); + }, /fatal parsing error/iu); + }); + + // https://github.com/eslint/eslint/issues/4779 + it("should throw an error if there's a parsing error and output doesn't match", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [], + invalid: [ + { code: "obvious parser error", output: "string that doesnt match", errors: [{}] } + ] + }); + }, /fatal parsing error/iu); + }); + + it("should not throw an error if invalid code has at least an expected empty error object", () => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: ["Eval(foo)"], + invalid: [{ + code: "eval(foo)", + errors: [{}] + }] + }); + }); + + it("should pass-through the globals config of valid tests to the to rule", () => { + ruleTester.run("no-test-global", require("./fixtures/no-test-global"), { + valid: [ + "var test = 'foo'", + { + code: "var test2 = 'bar'", + globals: { test: true } + } ], - }, - ); - }, 'Error Suggestion at index 0 : desc should be "not right" but got "Rename identifier \'foo\' to \'bar\'" instead.'); + invalid: [{ code: "bar", errors: 1 }] + }); }); - it("should throw if the suggestion description doesn't match (although messageIds match)", () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], + it("should pass-through the globals config of invalid tests to the to rule", () => { + ruleTester.run("no-test-global", require("./fixtures/no-test-global"), { + valid: ["var test = 'foo'"], invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - desc: "Rename identifier 'foo' to 'bar'", - messageId: 'renameFoo', - output: 'var bar;', - }, - { - desc: "Rename id 'foo' to 'baz'", - messageId: 'renameFoo', - output: 'var baz;', - }, - ], - }, - ], - }, + { + code: "var test = 'foo'; var foo = 'bar'", + errors: 1 + }, + { + code: "var test = 'foo'", + globals: { foo: true }, + errors: [{ message: "Global variable foo should not be used." }] + } + ] + }); + }); + + it("should pass-through the settings config to rules", () => { + ruleTester.run("no-test-settings", require("./fixtures/no-test-settings"), { + valid: [ + { + code: "var test = 'bar'", settings: { test: 1 } + } ], - }, - ); - }, "Error Suggestion at index 1 : desc should be \"Rename id 'foo' to 'baz'\" but got \"Rename identifier 'foo' to 'baz'\" instead."); + invalid: [ + { + code: "var test = 'bar'", settings: { "no-test": 22 }, errors: 1 + } + ] + }); }); - it("should throw if the suggestion messageId doesn't match", () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - messageId: 'unused', - output: 'var bar;', - }, - { - messageId: 'renameFoo', - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }, "Error Suggestion at index 0 : messageId should be 'unused' but got 'renameFoo' instead."); - }); + it("should pass-through the filename to the rule", () => { + (function() { + ruleTester.run("", require("./fixtures/no-test-filename"), { + valid: [ + { + code: "var foo = 'bar'", + filename: "somefile.js" + } + ], + invalid: [ + { + code: "var foo = 'bar'", + errors: [ + { message: "Filename test was not defined." } + ] + } + ] + }); + }()); + }); + + it("should pass-through the options to the rule", () => { + ruleTester.run("no-invalid-args", require("./fixtures/no-invalid-args"), { + valid: [ + { + code: "var foo = 'bar'", + options: [false] + } + ], + invalid: [ + { + code: "var foo = 'bar'", + options: [true], + errors: [{ message: "Invalid args" }] + } + ] + }); + }); + + it("should throw an error if the options are an object", () => { + assert.throws(() => { + ruleTester.run("no-invalid-args", require("./fixtures/no-invalid-args"), { + valid: [ + { + code: "foo", + options: { ok: true } + } + ], + invalid: [] + }); + }, /options must be an array/u); + }); + + it("should throw an error if the options are a number", () => { + assert.throws(() => { + ruleTester.run("no-invalid-args", require("./fixtures/no-invalid-args"), { + valid: [ + { + code: "foo", + options: 0 + } + ], + invalid: [] + }); + }, /options must be an array/u); + }); + + it("should pass-through the parser to the rule", () => { + const spy = sinon.spy(ruleTester.linter, "verify"); + + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + { + code: "Eval(foo)" + } + ], + invalid: [ + { + code: "eval(foo)", + parser: require.resolve("esprima"), + errors: [{ line: 1 }] + } + ] + }); + assert.strictEqual(spy.args[1][1].parser, require.resolve("esprima")); + }); + + // skipping because it's not something our parser cares about + it.skip("should pass normalized ecmaVersion to the rule", () => { + const reportEcmaVersionRule = { + meta: { + messages: { + ecmaVersionMessage: "context.parserOptions.ecmaVersion is {{type}} {{ecmaVersion}}." + } + }, + create: context => ({ + Program(node) { + const { ecmaVersion } = context.parserOptions; + + context.report({ + node, + messageId: "ecmaVersionMessage", + data: { type: typeof ecmaVersion, ecmaVersion } + }); + } + }) + }; + + const notEspree = require.resolve("./fixtures/empty-program-parser"); + + ruleTester.run("report-ecma-version", reportEcmaVersionRule, { + valid: [], + invalid: [ + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }] + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + parserOptions: {} + }, + { + code: "
", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + parserOptions: { ecmaFeatures: { jsx: true } } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + parser: require.resolve("espree") + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + env: { browser: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + env: { es6: false } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + env: { es6: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "8" } }], + env: { es6: false, es2017: true } + }, + { + code: "let x", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + env: { es6: "truthy" } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "8" } }], + env: { es2017: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "11" } }], + env: { es2020: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "12" } }], + env: { es2021: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], + parserOptions: { ecmaVersion: "latest" } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], + parser: require.resolve("espree"), + parserOptions: { ecmaVersion: "latest" } + }, + { + code: "
", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], + parserOptions: { ecmaVersion: "latest", ecmaFeatures: { jsx: true } } + }, + { + code: "import 'foo'", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], + parserOptions: { ecmaVersion: "latest", sourceType: "module" } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], + parserOptions: { ecmaVersion: "latest" }, + env: { es6: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], + parserOptions: { ecmaVersion: "latest" }, + env: { es2020: true } + }, + + // Non-Espree parsers normalize ecmaVersion if it's not "latest" + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + parser: notEspree + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + parser: notEspree, + parserOptions: {} + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "5" } }], + parser: notEspree, + parserOptions: { ecmaVersion: 5 } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + parser: notEspree, + parserOptions: { ecmaVersion: 6 } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: 6 } }], + parser: notEspree, + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "string", ecmaVersion: "latest" } }], + parser: notEspree, + parserOptions: { ecmaVersion: "latest" } + } + ] + }); + + [{ parserOptions: { ecmaVersion: 6 } }, { env: { es6: true } }].forEach(options => { + new RuleTester(options).run("report-ecma-version", reportEcmaVersionRule, { + valid: [], + invalid: [ + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }] + }, + { + code: "", + parserOptions: {}, + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }] + } + ] + }); + }); + + new RuleTester({ parser: notEspree }).run("report-ecma-version", reportEcmaVersionRule, { + valid: [], + invalid: [ + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }] + }, + { + code: "", + parserOptions: { ecmaVersion: "latest" }, + errors: [{ messageId: "ecmaVersionMessage", data: { type: "string", ecmaVersion: "latest" } }] + } + ] + }); + }); + + it("should pass-through services from parseForESLint to the rule", () => { + const enhancedParserPath = require.resolve("./fixtures/enhanced-parser"); + const disallowHiRule = { + create: context => ({ + Literal(node) { + const disallowed = context.parserServices.test.getMessage(); // returns "Hi!" + + if (node.value === disallowed) { + context.report({ node, message: `Don't use '${disallowed}'` }); + } + } + }) + }; + + ruleTester.run("no-hi", disallowHiRule, { + valid: [ + { + code: "'Hello!'", + parser: enhancedParserPath + } + ], + invalid: [ + { + code: "'Hi!'", + parser: enhancedParserPath, + errors: [{ message: "Don't use 'Hi!'" }] + } + ] + }); + }); + + it("should prevent invalid options schemas", () => { + assert.throws(() => { + ruleTester.run("no-invalid-schema", require("./fixtures/no-invalid-schema"), { + valid: [ + "var answer = 6 * 7;", + { code: "var answer = 6 * 7;", options: [] } + ], + invalid: [ + { code: "var answer = 6 * 7;", options: ["bar"], errors: [{ message: "Expected nothing." }] } + ] + }); + }, "Schema for rule no-invalid-schema is invalid:,\titems: should be object\n\titems[0].enum: should NOT have fewer than 1 items\n\titems: should match some schema in anyOf"); + + }); + + it("should prevent schema violations in options", () => { + assert.throws(() => { + ruleTester.run("no-schema-violation", require("./fixtures/no-schema-violation"), { + valid: [ + "var answer = 6 * 7;", + { code: "var answer = 6 * 7;", options: ["foo"] } + ], + invalid: [ + { code: "var answer = 6 * 7;", options: ["bar"], errors: [{ message: "Expected foo." }] } + ] + }); + }, /Value "bar" should be equal to one of the allowed values./u); + + }); + + it("should disallow invalid defaults in rules", () => { + const ruleWithInvalidDefaults = { + meta: { + schema: [ + { + oneOf: [ + { enum: ["foo"] }, + { + type: "object", + properties: { + foo: { + enum: ["foo", "bar"], + default: "foo" + } + }, + additionalProperties: false + } + ] + } + ] + }, + create: () => ({}) + }; + + assert.throws(() => { + ruleTester.run("invalid-defaults", ruleWithInvalidDefaults, { + valid: [ + { + code: "foo", + options: [{}] + } + ], + invalid: [] + }); + }, /Schema for rule invalid-defaults is invalid: default is ignored for: data1\.foo/u); + }); + + it("throw an error when an unknown config option is included", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + { code: "Eval(foo)", foo: "bar" } + ], + invalid: [] + }); + }, /ESLint configuration in rule-tester is invalid./u); + }); + + it("throw an error when an invalid config value is included", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: [ + { code: "Eval(foo)", env: ["es6"] } + ], + invalid: [] + }); + }, /Property "env" is the wrong type./u); + }); + + it("should pass-through the tester config to the rule", () => { + ruleTester = new RuleTester({ + globals: { test: true } + }); + + ruleTester.run("no-test-global", require("./fixtures/no-test-global"), { + valid: [ + "var test = 'foo'", + "var test2 = test" + ], + invalid: [{ code: "bar", errors: 1, globals: { foo: true } }] + }); + }); + + it("should correctly set the globals configuration", () => { + const config = { globals: { test: true } }; + + RuleTester.setDefaultConfig(config); + assert( + RuleTester.getDefaultConfig().globals.test, + "The default config object is incorrect" + ); + }); + + it("should correctly reset the global configuration", () => { + const config = { globals: { test: true } }; + + RuleTester.setDefaultConfig(config); + RuleTester.resetDefaultConfig(); + assert.deepStrictEqual( + RuleTester.getDefaultConfig(), + { parser: require.resolve('@typescript-eslint/parser'), rules: {} }, + "The default configuration has not reset correctly" + ); + }); + + it("should enforce the global configuration to be an object", () => { + + /** + * Set the default config for the rules tester + * @param {Object} config configuration object + * @returns {Function} Function to be executed + * @private + */ + function setConfig(config) { + return function() { + RuleTester.setDefaultConfig(config); + }; + } + assert.throw(setConfig()); + assert.throw(setConfig(1)); + assert.throw(setConfig(3.14)); + assert.throw(setConfig("foo")); + assert.throw(setConfig(null)); + assert.throw(setConfig(true)); + }); + + it("should pass-through the globals config to the tester then to the to rule", () => { + const config = { globals: { test: true } }; + + RuleTester.setDefaultConfig(config); + ruleTester = new RuleTester(); + + ruleTester.run("no-test-global", require("./fixtures/no-test-global"), { + valid: [ + "var test = 'foo'", + "var test2 = test" + ], + invalid: [{ code: "bar", errors: 1, globals: { foo: true } }] + }); + }); + + it("should throw an error if AST was modified", () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast"), { + valid: [ + "var foo = 0;" + ], + invalid: [] + }); + }, "Rule should not modify AST."); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast"), { + valid: [], + invalid: [ + { code: "var bar = 0;", errors: ["error"] } + ] + }); + }, "Rule should not modify AST."); + }); + + it("should throw an error if AST was modified (at Program)", () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast-at-first"), { + valid: [ + "var foo = 0;" + ], + invalid: [] + }); + }, "Rule should not modify AST."); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast-at-first"), { + valid: [], + invalid: [ + { code: "var bar = 0;", errors: ["error"] } + ] + }); + }, "Rule should not modify AST."); + }); + + it("should throw an error if AST was modified (at Program:exit)", () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), { + valid: [ + "var foo = 0;" + ], + invalid: [] + }); + }, "Rule should not modify AST."); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), { + valid: [], + invalid: [ + { code: "var bar = 0;", errors: ["error"] } + ] + }); + }, "Rule should not modify AST."); + }); + + it("should throw an error if rule uses start and end properties on nodes, tokens or comments", () => { + const usesStartEndRule = { + create(context) { + return { + CallExpression(node) { + noop(node.arguments[1].start); + }, + "BinaryExpression[operator='+']"(node) { + noop(node.end); + }, + "UnaryExpression[operator='-']"(node) { + noop(context.sourceCode.getFirstToken(node).start); + }, + ConditionalExpression(node) { + noop(context.sourceCode.getFirstToken(node).end); + }, + BlockStatement(node) { + noop(context.sourceCode.getCommentsInside(node)[0].start); + }, + ObjectExpression(node) { + noop(context.sourceCode.getCommentsInside(node)[0].end); + }, + Decorator(node) { + noop(node.start); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: ["foo(a, b)"], + invalid: [] + }); + }, "Use node.range[0] instead of node.start"); + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: [], + invalid: [{ code: "var a = b * (c + d) / e;", errors: 1 }] + }); + }, "Use node.range[1] instead of node.end"); + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: [], + invalid: [{ code: "var a = -b * c;", errors: 1 }] + }); + }, "Use token.range[0] instead of token.start"); + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: ["var a = b ? c : d;"], + invalid: [] + }); + }, "Use token.range[1] instead of token.end"); + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: ["function f() { /* comment */ }"], + invalid: [] + }); + }, "Use token.range[0] instead of token.start"); + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: [], + invalid: [{ code: "var x = //\n {\n //comment\n //\n}", errors: 1 }] + }); + }, "Use token.range[1] instead of token.end"); + + const enhancedParserPath = require.resolve("./fixtures/enhanced-parser"); + + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: [{ code: "foo(a, b)", parser: enhancedParserPath }], + invalid: [] + }); + }, "Use node.range[0] instead of node.start"); + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: [], + invalid: [{ code: "var a = b * (c + d) / e;", parser: enhancedParserPath, errors: 1 }] + }); + }, "Use node.range[1] instead of node.end"); + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: [], + invalid: [{ code: "var a = -b * c;", parser: enhancedParserPath, errors: 1 }] + }); + }, "Use token.range[0] instead of token.start"); + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: [{ code: "var a = b ? c : d;", parser: enhancedParserPath }], + invalid: [] + }); + }, "Use token.range[1] instead of token.end"); + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: [{ code: "function f() { /* comment */ }", parser: enhancedParserPath }], + invalid: [] + }); + }, "Use token.range[0] instead of token.start"); + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: [], + invalid: [{ code: "var x = //\n {\n //comment\n //\n}", parser: enhancedParserPath, errors: 1 }] + }); + }, "Use token.range[1] instead of token.end"); + + assert.throws(() => { + ruleTester.run("uses-start-end", usesStartEndRule, { + valid: [{ code: "@foo class A {}", parser: require.resolve("./fixtures/enhanced-parser2") }], + invalid: [] + }); + }, "Use node.range[0] instead of node.start"); + }); + + it("should throw an error if no test scenarios given", () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast-at-last")); + }, "Test Scenarios for rule foo : Could not find test scenario object"); + }); + + it("should throw an error if no acceptable test scenario object is given", () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), []); + }, "Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios\nCould not find any invalid test scenarios"); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), ""); + }, "Test Scenarios for rule foo : Could not find test scenario object"); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), 2); + }, "Test Scenarios for rule foo : Could not find test scenario object"); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), {}); + }, "Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios\nCould not find any invalid test scenarios"); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), { + valid: [] + }); + }, "Test Scenarios for rule foo is invalid:\nCould not find any invalid test scenarios"); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), { + invalid: [] + }); + }, "Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios"); + }); + + // Nominal message/messageId use cases + it("should assert match if message provided in both test and result.", () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMessageOnly, { + valid: [], + invalid: [{ code: "foo", errors: [{ message: "something" }] }] + }); + }, /Avoid using variables named/u); + + ruleTester.run("foo", require("./fixtures/messageId").withMessageOnly, { + valid: [], + invalid: [{ code: "foo", errors: [{ message: "Avoid using variables named 'foo'." }] }] + }); + }); + + it("should assert match between messageId if provided in both test and result.", () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "unused" }] }] + }); + }, "messageId 'avoidFoo' does not match expected messageId 'unused'."); + + ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); + }); + it("should assert match between resulting message output if messageId and data provided in both test and result", () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "notFoo" } }] }] + }); + }, "Hydrated message \"Avoid using variables named 'notFoo'.\" does not match \"Avoid using variables named 'foo'.\""); + }); + + // messageId/message misconfiguration cases + it("should throw if user tests for both message and messageId", () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, { + valid: [], + invalid: [{ code: "foo", errors: [{ message: "something", messageId: "avoidFoo" }] }] + }); + }, "Error should not specify both 'message' and a 'messageId'."); + }); + it("should throw if user tests for messageId but the rule doesn't use the messageId meta syntax.", () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMessageOnly, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); + }, "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'"); + }); + it("should throw if user tests for messageId not listed in the rule's meta syntax.", () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "useFoo" }] }] + }); + }, /Invalid messageId 'useFoo'/u); + }); + it("should throw if data provided without messageId.", () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, { + valid: [], + invalid: [{ code: "foo", errors: [{ data: "something" }] }] + }); + }, "Error must specify 'messageId' if 'data' is used."); + }); + + describe("suggestions", () => { + it("should pass with valid suggestions (tested using desc)", () => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [ + "var boo;" + ], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var bar;" + }] + }] + }] + }); + }); + + it("should pass with suggestions on multiple lines", () => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [], + invalid: [ + { + code: "function foo() {\n var foo = 1;\n}", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "function bar() {\n var foo = 1;\n}" + }] + }, { + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "function foo() {\n var bar = 1;\n}" + }] + }] + } + ] + }); + }); + + it("should pass with valid suggestions (tested using messageIds)", () => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "renameFoo", + output: "var bar;" + }, { + messageId: "renameFoo", + output: "var baz;" + }] + }] + }] + }); + }); + + it("should pass with valid suggestions (one tested using messageIds, the other using desc)", () => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "renameFoo", + output: "var bar;" + }, { + desc: "Rename identifier 'foo' to 'baz'", + output: "var baz;" + }] + }] + }] + }); + }); + + it("should pass with valid suggestions (tested using both desc and messageIds for the same suggestion)", () => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + messageId: "renameFoo", + output: "var bar;" + }, { + desc: "Rename identifier 'foo' to 'baz'", + messageId: "renameFoo", + output: "var baz;" + }] + }] + }] + }); + }); + + it("should pass with valid suggestions (tested using only desc on a rule that utilizes meta.messages)", () => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var bar;" + }, { + desc: "Rename identifier 'foo' to 'baz'", + output: "var baz;" + }] + }] + }] + }); + }); + + it("should pass with valid suggestions (tested using messageIds and data)", () => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "renameFoo", + data: { newName: "bar" }, + output: "var bar;" + }, { + messageId: "renameFoo", + data: { newName: "baz" }, + output: "var baz;" + }] + }] + }] + }); + }); + + + it("should pass when tested using empty suggestion test objects if the array length is correct", () => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{}, {}] + }] + }] + }); + }); + + it("should support explicitly expecting no suggestions", () => { + [void 0, null, false, []].forEach(suggestions => { + ruleTester.run("suggestions-basic", require("./fixtures/no-eval"), { + valid: [], + invalid: [{ + code: "eval('var foo');", + errors: [{ + suggestions + }] + }] + }); + }); + }); + + it("should fail when expecting no suggestions and there are suggestions", () => { + [void 0, null, false, []].forEach(suggestions => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions + }] + }] + }); + }, "Error should have no suggestions on error with message: \"Avoid using identifiers named 'foo'.\""); + }); + }); + + it("should fail when testing for suggestions that don't exist", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "this-does-not-exist" + }] + }] + }] + }); + }, "Error should have an array of suggestions. Instead received \"undefined\" on error with message: \"Bad var.\""); + }); + + it("should fail when there are a different number of suggestions", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var bar;" + }, { + desc: "Rename identifier 'foo' to 'baz'", + output: "var baz;" + }] + }] + }] + }); + }, "Error should have 2 suggestions. Instead found 1 suggestions"); + }); + + it("should throw if suggestion fix made a syntax error.", () => { + assert.throw(() => { + ruleTester.run( + "foo", + { + meta: { hasSuggestions: true }, + create(context) { + return { + Identifier(node) { + context.report({ + node, + message: "make a syntax error", + suggest: [ + { + desc: "make a syntax error", + fix(fixer) { + return fixer.replaceText(node, "one two"); + } + } + ] + }); + } + }; + } + }, + { + valid: [""], + invalid: [{ + code: "one()", + errors: [{ + message: "make a syntax error", + suggestions: [{ + desc: "make a syntax error", + output: "one two()" + }] + }] + }] + } + ); + }, /A fatal parsing error occurred in suggestion fix\.\nError: .+\nSuggestion output:\n.+/u); + }); + + it("should throw if the suggestion description doesn't match", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "not right", + output: "var baz;" + }] + }] + }] + }); + }, "Error Suggestion at index 0 : desc should be \"not right\" but got \"Rename identifier 'foo' to 'bar'\" instead."); + }); + + it("should throw if the suggestion description doesn't match (although messageIds match)", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + messageId: "renameFoo", + output: "var bar;" + }, { + desc: "Rename id 'foo' to 'baz'", + messageId: "renameFoo", + output: "var baz;" + }] + }] + }] + }); + }, "Error Suggestion at index 1 : desc should be \"Rename id 'foo' to 'baz'\" but got \"Rename identifier 'foo' to 'baz'\" instead."); + }); + + it("should throw if the suggestion messageId doesn't match", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "unused", + output: "var bar;" + }, { + messageId: "renameFoo", + output: "var baz;" + }] + }] + }] + }); + }, "Error Suggestion at index 0 : messageId should be 'unused' but got 'renameFoo' instead."); + }); + + it("should throw if the suggestion messageId doesn't match (although descriptions match)", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + messageId: "renameFoo", + output: "var bar;" + }, { + desc: "Rename identifier 'foo' to 'baz'", + messageId: "avoidFoo", + output: "var baz;" + }] + }] + }] + }); + }, "Error Suggestion at index 1 : messageId should be 'avoidFoo' but got 'renameFoo' instead."); + }); + + it("should throw if test specifies messageId for a rule that doesn't have meta.messages", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "renameFoo", + output: "var bar;" + }] + }] + }] + }); + }, "Error Suggestion at index 0 : Test can not use 'messageId' if rule under test doesn't define 'meta.messages'."); + }); + + it("should throw if test specifies messageId that doesn't exist in the rule's meta.messages", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "renameFoo", + output: "var bar;" + }, { + messageId: "removeFoo", + output: "var baz;" + }] + }] + }] + }); + }, "Error Suggestion at index 1 : Test has invalid messageId 'removeFoo', the rule under test allows only one of ['avoidFoo', 'unused', 'renameFoo']."); + }); + + it("should throw if hydrated desc doesn't match (wrong data value)", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "renameFoo", + data: { newName: "car" }, + output: "var bar;" + }, { + messageId: "renameFoo", + data: { newName: "baz" }, + output: "var baz;" + }] + }] + }] + }); + }, "Error Suggestion at index 0 : Hydrated test desc \"Rename identifier 'foo' to 'car'\" does not match received desc \"Rename identifier 'foo' to 'bar'\"."); + }); + + it("should throw if hydrated desc doesn't match (wrong data key)", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "renameFoo", + data: { newName: "bar" }, + output: "var bar;" + }, { + messageId: "renameFoo", + data: { name: "baz" }, + output: "var baz;" + }] + }] + }] + }); + }, "Error Suggestion at index 1 : Hydrated test desc \"Rename identifier 'foo' to '{{ newName }}'\" does not match received desc \"Rename identifier 'foo' to 'baz'\"."); + }); + + it("should throw if test specifies both desc and data", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + messageId: "renameFoo", + data: { newName: "bar" }, + output: "var bar;" + }, { + messageId: "renameFoo", + data: { newName: "baz" }, + output: "var baz;" + }] + }] + }] + }); + }, "Error Suggestion at index 0 : Test should not specify both 'desc' and 'data'."); + }); + + it("should throw if test uses data but doesn't specify messageId", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "renameFoo", + data: { newName: "bar" }, + output: "var bar;" + }, { + data: { newName: "baz" }, + output: "var baz;" + }] + }] + }] + }); + }, "Error Suggestion at index 1 : Test must specify 'messageId' if 'data' is used."); + }); + + it("should throw if the resulting suggestion output doesn't match", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var baz;" + }] + }] + }] + }); + }, "Expected the applied suggestion fix to match the test suggestion output"); + }); + + it("should fail when specified suggestion isn't an object", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [null] + }] + }] + }); + }, "Test suggestion in 'suggestions' array must be an object."); + + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [ + { + messageId: "renameFoo", + output: "var bar;" + }, + "Rename identifier 'foo' to 'baz'" + ] + }] + }] + }); + }, "Test suggestion in 'suggestions' array must be an object."); + }); + + it("should fail when the suggestion is an object with an unknown property name", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [ + "var boo;" + ], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + message: "Rename identifier 'foo' to 'bar'" + }] + }] + }] + }); + }, /Invalid suggestion property name 'message'/u); + }); + + it("should fail when any of the suggestions is an object with an unknown property name", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "renameFoo", + output: "var bar;" + }, { + messageId: "renameFoo", + outpt: "var baz;" + }] + }] + }] + }); + }, /Invalid suggestion property name 'outpt'/u); + }); + + it("should fail if a rule produces two suggestions with the same description", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-descriptions", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateDescriptions, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + + it("should fail if a rule produces two suggestions with the same messageId without data", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-messageids-no-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsNoData, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename identifier' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + + it("should fail if a rule produces two suggestions with the same messageId with data", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-messageids-with-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsWithData, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename identifier 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + + it("should throw an error if a rule that doesn't have `meta.hasSuggestions` enabled produces suggestions", () => { + assert.throws(() => { + ruleTester.run("suggestions-missing-hasSuggestions-property", require("./fixtures/suggestions").withoutHasSuggestionsProperty, { + valid: [], + invalid: [ + { code: "var foo = bar;", output: "5", errors: 1 } + ] + }); + }, "Rules with suggestions must set the `meta.hasSuggestions` property to `true`."); + }); + }); + + 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/latest/extend/custom-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/latest/extend/custom-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/latest/extend/custom-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 -- intentionally added for test case + schema: undefined + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; - it("should throw if the suggestion messageId doesn't match (although descriptions match)", () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - desc: "Rename identifier 'foo' to 'bar'", - messageId: 'renameFoo', - output: 'var bar;', - }, - { - desc: "Rename identifier 'foo' to 'baz'", - messageId: 'avoidFoo', - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }, "Error Suggestion at index 1 : messageId should be 'avoidFoo' but got 'renameFoo' instead."); - }); + ruleTester.run("rule-with-undefined-schema", ruleWithUndefinedSchema, { + valid: [], + invalid: [ + { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } + ] + }); - it("should throw if test specifies messageId for a rule that doesn't have meta.messages", () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-basic', - require('./fixtures/suggestions').basic, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - messageId: 'renameFoo', - output: 'var bar;', - }, - ], - }, - ], - }, - ], - }, - ); - }, "Error Suggestion at index 0 : Test can not use 'messageId' if rule under test doesn't define 'meta.messages'."); - }); + 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/latest/extend/custom-rules#options-schemas", + "DeprecationWarning" + ] + ); + }); - it("should throw if test specifies messageId that doesn't exist in the rule's meta.messages", () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - messageId: 'renameFoo', - output: 'var bar;', - }, - { - messageId: 'removeFoo', - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }, "Error Suggestion at index 1 : Test has invalid messageId 'removeFoo', the rule under test allows only one of ['avoidFoo', 'unused', 'renameFoo']."); - }); + 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" }); + } + }; + } + }; - it("should throw if hydrated desc doesn't match (wrong data value)", () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - messageId: 'renameFoo', - data: { newName: 'car' }, - output: 'var bar;', - }, - { - messageId: 'renameFoo', - data: { newName: 'baz' }, - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }, "Error Suggestion at index 0 : Hydrated test desc \"Rename identifier 'foo' to 'car'\" does not match received desc \"Rename identifier 'foo' to 'bar'\"."); - }); + ruleTester.run("rule-with-null-schema", ruleWithNullSchema, { + valid: [], + invalid: [ + { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } + ] + }); - it("should throw if hydrated desc doesn't match (wrong data key)", () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - messageId: 'renameFoo', - data: { newName: 'bar' }, - output: 'var bar;', - }, - { - messageId: 'renameFoo', - data: { name: 'baz' }, - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }, "Error Suggestion at index 1 : Hydrated test desc \"Rename identifier 'foo' to '{{ newName }}'\" does not match received desc \"Rename identifier 'foo' to 'baz'\"."); - }); + 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/latest/extend/custom-rules#options-schemas", + "DeprecationWarning" + ] + ); + }); - it('should throw if test specifies both desc and data', () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - desc: "Rename identifier 'foo' to 'bar'", - messageId: 'renameFoo', - data: { newName: 'bar' }, - output: 'var bar;', - }, - { - messageId: 'renameFoo', - data: { newName: 'baz' }, - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }, "Error Suggestion at index 0 : Test should not specify both 'desc' and 'data'."); - }); + 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" }); + } + }; + } + }; - it("should throw if test uses data but doesn't specify messageId", () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - messageId: 'renameFoo', - data: { newName: 'bar' }, - output: 'var bar;', - }, - { - data: { newName: 'baz' }, - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }, "Error Suggestion at index 1 : Test must specify 'messageId' if 'data' is used."); - }); + ruleTester.run("rule-with-no-options", ruleWithEmptySchema, { + valid: [], + invalid: [{ code: "var foo = bar;", errors: 1 }] + }); - it("should throw if the resulting suggestion output doesn't match", () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-basic', - require('./fixtures/suggestions').basic, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - desc: "Rename identifier 'foo' to 'bar'", - output: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }, 'Expected the applied suggestion fix to match the test suggestion output'); - }); + assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); + }); - it("should fail when specified suggestion isn't an object", () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-basic', - require('./fixtures/suggestions').basic, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [null], - }, - ], - }, - ], - }, - ); - }, "Test suggestion in 'suggestions' array must be an object."); + 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.throws(() => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - messageId: 'renameFoo', - output: 'var bar;', - }, - "Rename identifier 'foo' to 'baz'", - ], - }, - ], - }, - ], - }, - ); - }, "Test suggestion in 'suggestions' array must be an object."); - }); + assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); + }); - it('should fail when the suggestion is an object with an unknown property name', () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-basic', - require('./fixtures/suggestions').basic, - { - valid: ['var boo;'], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - message: "Rename identifier 'foo' to 'bar'", - }, - ], - }, - ], - }, - ], - }, - ); - }, /Invalid suggestion property name 'message'/u); - }); + 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" }); + } + }; + } + }; - it('should fail when any of the suggestions is an object with an unknown property name', () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-messageIds', - require('./fixtures/suggestions').withMessageIds, - { - valid: [], - invalid: [ - { - code: 'var foo;', - errors: [ - { - suggestions: [ - { - messageId: 'renameFoo', - output: 'var bar;', - }, - { - messageId: 'renameFoo', - outpt: 'var baz;', - }, - ], - }, - ], - }, - ], - }, - ); - }, /Invalid suggestion property name 'outpt'/u); - }); + ruleTester.run("rule-with-schema", ruleWithSchema, { + valid: [], + invalid: [ + { code: "var foo = bar;", options: [true], errors: 1 } + ] + }); - it('should fail if a rule produces two suggestions with the same description', () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-with-duplicate-descriptions', - require('../../fixtures/testers/rule-tester/suggestions') - .withDuplicateDescriptions, - { - valid: [], - invalid: [{ code: 'var foo = bar;', errors: 1 }], - }, - ); - }, "Suggestion message 'Rename 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); - }); + assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); + }); - it('should fail if a rule produces two suggestions with the same messageId without data', () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-with-duplicate-messageids-no-data', - require('../../fixtures/testers/rule-tester/suggestions') - .withDuplicateMessageIdsNoData, - { - valid: [], - invalid: [{ code: 'var foo = bar;', errors: 1 }], - }, - ); - }, "Suggestion message 'Rename identifier' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); - }); + 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 } + ] + }); - it('should fail if a rule produces two suggestions with the same messageId with data', () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-with-duplicate-messageids-with-data', - require('../../fixtures/testers/rule-tester/suggestions') - .withDuplicateMessageIdsWithData, - { - valid: [], - invalid: [{ code: 'var foo = bar;', errors: 1 }], - }, - ); - }, "Suggestion message 'Rename identifier 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); - }); + assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); + }); - it("should throw an error if a rule that doesn't have `meta.hasSuggestions` enabled produces suggestions", () => { - assert.throws(() => { - ruleTester.run( - 'suggestions-missing-hasSuggestions-property', - require('./fixtures/suggestions').withoutHasSuggestionsProperty, - { - valid: [], - invalid: [{ code: 'var foo = bar;', output: '5', errors: 1 }], - }, - ); - }, 'Rules with suggestions must set the `meta.hasSuggestions` property to `true`.'); - }); - }); - - 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' }); - }, - }; - }, - }; + 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 } + ] + }); - beforeEach(() => { - processStub = sinon.stub(process, 'emitWarning'); - }); + 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 } + ] + }); - afterEach(() => { - processStub.restore(); + assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); + }); }); - 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' }); - }, - }; - } + /** + * Asserts that a particular value will be emitted from an EventEmitter. + * @param {EventEmitter} emitter The emitter that should emit a value + * @param {string} emitType The type of emission to listen for + * @param {any} expectedValue The value that should be emitted + * @returns {Promise} A Promise that fulfills if the value is emitted, and rejects if something else is emitted. + * The Promise will be indefinitely pending if no value is emitted. + */ + function assertEmitted(emitter, emitType, expectedValue) { + return new Promise((resolve, reject) => { + emitter.once(emitType, emittedValue => { + if (emittedValue === expectedValue) { + resolve(); + } else { + reject(new Error(`Expected ${expectedValue} to be emitted but ${emittedValue} was emitted instead.`)); + } + }); + }); + } - ruleTester.run('function-style-rule', functionStyleRule, { - valid: [], - invalid: [{ code: 'var foo = bar;', errors: 1 }], - }); + describe("naming test cases", () => { - 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/latest/extend/custom-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 }, - ], - }); + it("should use the first argument as the name of the test suite", () => { + const assertion = assertEmitted(ruleTesterTestEmitter, "describe", "this-is-a-rule-name"); - 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/latest/extend/custom-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 }, - ], - }); + ruleTester.run("this-is-a-rule-name", require("./fixtures/no-var"), { + valid: [], + invalid: [] + }); - 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/latest/extend/custom-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 -- intentionally 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 }, - ], - }); + return assertion; + }); - 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/latest/extend/custom-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 }, - ], - }); + it("should use the test code as the name of the tests for valid code (string form)", () => { + const assertion = assertEmitted(ruleTesterTestEmitter, "it", "valid(code);"); - 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/latest/extend/custom-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("foo", require("./fixtures/no-var"), { + valid: [ + "valid(code);" + ], + invalid: [] + }); - ruleTester.run('rule-with-no-options', ruleWithEmptySchema, { - valid: [], - invalid: [{ code: 'var foo = bar;', errors: 1 }], - }); + return assertion; + }); - assert.strictEqual( - processStub.callCount, - 0, - 'never calls `process.emitWarning()`', - ); - }); + it("should use the test code as the name of the tests for valid code (object form)", () => { + const assertion = assertEmitted(ruleTesterTestEmitter, "it", "valid(code);"); - 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 }], - }); + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [ + { + code: "valid(code);" + } + ], + invalid: [] + }); - assert.strictEqual( - processStub.callCount, - 0, - 'never calls `process.emitWarning()`', - ); - }); + return assertion; + }); - 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' }); - }, - }; - }, - }; + it("should use the test code as the name of the tests for invalid code", () => { + const assertion = assertEmitted(ruleTesterTestEmitter, "it", "var x = invalid(code);"); - ruleTester.run('rule-with-schema', ruleWithSchema, { - valid: [], - invalid: [{ code: 'var foo = bar;', options: [true], errors: 1 }], - }); + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [], + invalid: [ + { + code: "var x = invalid(code);", + output: " x = invalid(code);", + errors: 1 + } + ] + }); - assert.strictEqual( - processStub.callCount, - 0, - 'never calls `process.emitWarning()`', - ); - }); + return assertion; + }); - 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 }], - }); + // https://github.com/eslint/eslint/issues/8142 + it("should use the empty string as the name of the test if the test case is an empty string", () => { + const assertion = assertEmitted(ruleTesterTestEmitter, "it", ""); - assert.strictEqual( - processStub.callCount, - 0, - 'never calls `process.emitWarning()`', - ); - }); + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [ + { + code: "" + } + ], + invalid: [] + }); - 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 }], - }); + return assertion; + }); - 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 }], - }); + it('should use the "name" property if set to a non-empty string', () => { + const assertion = assertEmitted(ruleTesterTestEmitter, "it", "my test"); - 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 - * @param {string} emitType The type of emission to listen for - * @param {any} expectedValue The value that should be emitted - * @returns {Promise} A Promise that fulfills if the value is emitted, and rejects if something else is emitted. - * The Promise will be indefinitely pending if no value is emitted. - */ - function assertEmitted(emitter, emitType, expectedValue) { - return new Promise((resolve, reject) => { - emitter.once(emitType, emittedValue => { - if (emittedValue === expectedValue) { - resolve(); - } else { - reject( - new Error( - `Expected ${expectedValue} to be emitted but ${emittedValue} was emitted instead.`, - ), - ); - } - }); - }); - } + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [], + invalid: [ + { + name: "my test", + code: "var x = invalid(code);", + output: " x = invalid(code);", + errors: 1 + } + ] + }); - describe('naming test cases', () => { - it('should use the first argument as the name of the test suite', () => { - const assertion = assertEmitted( - ruleTesterTestEmitter, - 'describe', - 'this-is-a-rule-name', - ); + return assertion; + }); - ruleTester.run('this-is-a-rule-name', require('./fixtures/no-var'), { - valid: [], - invalid: [], - }); + it('should use the "name" property if set to a non-empty string for valid cases too', () => { + const assertion = assertEmitted(ruleTesterTestEmitter, "it", "my test"); - return assertion; - }); + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [ + { + name: "my test", + code: "valid(code);" + } + ], + invalid: [] + }); - it('should use the test code as the name of the tests for valid code (string form)', () => { - const assertion = assertEmitted( - ruleTesterTestEmitter, - 'it', - 'valid(code);', - ); + return assertion; + }); - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: ['valid(code);'], - invalid: [], - }); - return assertion; - }); + it('should use the test code as the name if the "name" property is set to an empty string', () => { + const assertion = assertEmitted(ruleTesterTestEmitter, "it", "var x = invalid(code);"); - it('should use the test code as the name of the tests for valid code (object form)', () => { - const assertion = assertEmitted( - ruleTesterTestEmitter, - 'it', - 'valid(code);', - ); + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [], + invalid: [ + { + name: "", + code: "var x = invalid(code);", + output: " x = invalid(code);", + errors: 1 + } + ] + }); - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: [ - { - code: 'valid(code);', - }, - ], - invalid: [], - }); + return assertion; + }); - return assertion; - }); - - it('should use the test code as the name of the tests for invalid code', () => { - const assertion = assertEmitted( - ruleTesterTestEmitter, - 'it', - 'var x = invalid(code);', - ); - - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: [], - invalid: [ - { - code: 'var x = invalid(code);', - output: ' x = invalid(code);', - errors: 1, - }, - ], - }); + it('should throw if "name" property is not a string', () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [{ code: "foo", name: 123 }], + invalid: [{ code: "foo" }] - return assertion; - }); + }); + }, /Optional test case property 'name' must be a string/u); - // https://github.com/eslint/eslint/issues/8142 - it('should use the empty string as the name of the test if the test case is an empty string', () => { - const assertion = assertEmitted(ruleTesterTestEmitter, 'it', ''); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: ["foo"], + invalid: [{ code: "foo", name: 123 }] + }); + }, /Optional test case property 'name' must be a string/u); + }); - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: [ - { - code: '', - }, - ], - invalid: [], - }); + it('should throw if "code" property is not a string', () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [{ code: 123 }], + invalid: [{ code: "foo" }] - return assertion; - }); + }); + }, /Test case must specify a string value for 'code'/u); - it('should use the "name" property if set to a non-empty string', () => { - const assertion = assertEmitted(ruleTesterTestEmitter, 'it', 'my test'); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [123], + invalid: [{ code: "foo" }] - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: [], - invalid: [ - { - name: 'my test', - code: 'var x = invalid(code);', - output: ' x = invalid(code);', - errors: 1, - }, - ], - }); + }); + }, /Test case must specify a string value for 'code'/u); - return assertion; - }); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: ["foo"], + invalid: [{ code: 123 }] + }); + }, /Test case must specify a string value for 'code'/u); + }); - it('should use the "name" property if set to a non-empty string for valid cases too', () => { - const assertion = assertEmitted(ruleTesterTestEmitter, 'it', 'my test'); + it('should throw if "code" property is missing', () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [{ }], + invalid: [{ code: "foo" }] - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: [ - { - name: 'my test', - code: 'valid(code);', - }, - ], - invalid: [], - }); + }); + }, /Test case must specify a string value for 'code'/u); - return assertion; - }); - - it('should use the test code as the name if the "name" property is set to an empty string', () => { - const assertion = assertEmitted( - ruleTesterTestEmitter, - 'it', - 'var x = invalid(code);', - ); - - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: [], - invalid: [ - { - name: '', - code: 'var x = invalid(code);', - output: ' x = invalid(code);', - errors: 1, - }, - ], - }); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: ["foo"], + invalid: [{ }] + }); + }, /Test case must specify a string value for 'code'/u); + }); + }); - return assertion; + // https://github.com/eslint/eslint/issues/11615 + it("should fail the case if autofix made a syntax error.", () => { + assert.throw(() => { + ruleTester.run( + "foo", + { + meta: { + fixable: "code" + }, + create(context) { + return { + Identifier(node) { + context.report({ + node, + message: "make a syntax error", + fix(fixer) { + return fixer.replaceText(node, "one two"); + } + }); + } + }; + } + }, + { + valid: ["one()"], + invalid: [] + } + ); + }, /A fatal parsing error occurred in autofix.\nError: .+\nAutofix output:\n.+/u); }); - it('should throw if "name" property is not a string', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: [{ code: 'foo', name: 123 }], - invalid: [{ code: 'foo' }], - }); - }, /Optional test case property 'name' must be a string/u); + describe("sanitize test cases", () => { + let originalRuleTesterIt; + let spyRuleTesterIt; - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: ['foo'], - invalid: [{ code: 'foo', name: 123 }], + before(() => { + originalRuleTesterIt = RuleTester.it; + spyRuleTesterIt = sinon.spy(); + RuleTester.it = spyRuleTesterIt; }); - }, /Optional test case property 'name' must be a string/u); - }); + after(() => { + RuleTester.it = originalRuleTesterIt; + }); + beforeEach(() => { + spyRuleTesterIt.resetHistory(); + ruleTester = new RuleTester(); + }); + it("should present newline when using back-tick as new line", () => { + const code = ` + var foo = bar;`; - it('should throw if "code" property is not a string', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: [{ code: 123 }], - invalid: [{ code: 'foo' }], + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [], + invalid: [ + { + code, + errors: [/^Bad var/u] + } + ] + }); + sinon.assert.calledWith(spyRuleTesterIt, code); }); - }, /Test case must specify a string value for 'code'/u); + it("should present \\u0000 as a string", () => { + const code = "\u0000"; - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: [123], - invalid: [{ code: 'foo' }], + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [], + invalid: [ + { + code, + errors: [/^Bad var/u] + } + ] + }); + sinon.assert.calledWith(spyRuleTesterIt, "\\u0000"); }); - }, /Test case must specify a string value for 'code'/u); + it("should present the pipe character correctly", () => { + const code = "var foo = bar || baz;"; - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: ['foo'], - invalid: [{ code: 123 }], + ruleTester.run("no-var", require("./fixtures/no-var"), { + valid: [], + invalid: [ + { + code, + errors: [/^Bad var/u] + } + ] + }); + sinon.assert.calledWith(spyRuleTesterIt, code); }); - }, /Test case must specify a string value for 'code'/u); + }); - it('should throw if "code" property is missing', () => { - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: [{}], - invalid: [{ code: 'foo' }], + describe("SourceCode#getComments()", () => { + const useGetCommentsRule = { + create: context => ({ + Program(node) { + const sourceCode = context.getSourceCode(); + + sourceCode.getComments(node); + } + }) + }; + + it("should throw if called from a valid test case", () => { + assert.throws(() => { + ruleTester.run("use-get-comments", useGetCommentsRule, { + valid: [""], + invalid: [] + }); + }, /`SourceCode#getComments\(\)` is deprecated/u); }); - }, /Test case must specify a string value for 'code'/u); - assert.throws(() => { - ruleTester.run('foo', require('./fixtures/no-var'), { - valid: ['foo'], - invalid: [{}], + it("should throw if called from an invalid test case", () => { + assert.throws(() => { + ruleTester.run("use-get-comments", useGetCommentsRule, { + valid: [], + invalid: [{ + code: "", + errors: [{}] + }] + }); + }, /`SourceCode#getComments\(\)` is deprecated/u); }); - }, /Test case must specify a string value for 'code'/u); }); - }); - // https://github.com/eslint/eslint/issues/11615 - it('should fail the case if autofix made a syntax error.', () => { - assert.throw(() => { - ruleTester.run( - 'foo', - { - meta: { - fixable: 'code', - }, - create(context) { - return { - Identifier(node) { - context.report({ - node, - message: 'make a syntax error', - fix(fixer) { - return fixer.replaceText(node, 'one two'); - }, - }); - }, + describe("Subclassing", () => { + + it("should allow subclasses to set the describe/it/itOnly statics and should correctly use those values", () => { + const assertionDescribe = assertEmitted(ruleTesterTestEmitter, "custom describe", "this-is-a-rule-name"); + const assertionIt = assertEmitted(ruleTesterTestEmitter, "custom it", "valid(code);"); + const assertionItOnly = assertEmitted(ruleTesterTestEmitter, "custom itOnly", "validOnly(code);"); + + /** + * Subclass for testing + */ + class RuleTesterSubclass extends RuleTester { } + RuleTesterSubclass.describe = function(text, method) { + ruleTesterTestEmitter.emit("custom describe", text, method); + return method.call(this); + }; + RuleTesterSubclass.it = function(text, method) { + ruleTesterTestEmitter.emit("custom it", text, method); + return method.call(this); + }; + RuleTesterSubclass.itOnly = function(text, method) { + ruleTesterTestEmitter.emit("custom itOnly", text, method); + return method.call(this); }; - }, - }, - { - valid: ['one()'], - invalid: [], - }, - ); - }, /A fatal parsing error occurred in autofix.\nError: .+\nAutofix output:\n.+/u); - }); - - describe('sanitize test cases', () => { - let originalRuleTesterIt; - let spyRuleTesterIt; - before(() => { - originalRuleTesterIt = RuleTester.it; - spyRuleTesterIt = sinon.spy(); - RuleTester.it = spyRuleTesterIt; - }); - after(() => { - RuleTester.it = originalRuleTesterIt; - }); - beforeEach(() => { - spyRuleTesterIt.resetHistory(); - ruleTester = new RuleTester(); - }); - it('should present newline when using back-tick as new line', () => { - const code = ` - var foo = bar;`; + const ruleTesterSubclass = new RuleTesterSubclass(); - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [], - invalid: [ - { - code, - errors: [/^Bad var/u], - }, - ], - }); - sinon.assert.calledWith(spyRuleTesterIt, code); - }); - it('should present \\u0000 as a string', () => { - const code = '\u0000'; - - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [], - invalid: [ - { - code, - errors: [/^Bad var/u], - }, - ], - }); - sinon.assert.calledWith(spyRuleTesterIt, '\\u0000'); - }); - it('should present the pipe character correctly', () => { - const code = 'var foo = bar || baz;'; - - ruleTester.run('no-var', require('./fixtures/no-var'), { - valid: [], - invalid: [ - { - code, - errors: [/^Bad var/u], - }, - ], - }); - sinon.assert.calledWith(spyRuleTesterIt, code); - }); - }); - - describe('SourceCode#getComments()', () => { - const useGetCommentsRule = { - create: context => ({ - Program(node) { - const sourceCode = context.getSourceCode(); - - sourceCode.getComments(node); - }, - }), - }; - - it('should throw if called from a valid test case', () => { - assert.throws(() => { - ruleTester.run('use-get-comments', useGetCommentsRule, { - valid: [''], - invalid: [], - }); - }, /`SourceCode#getComments\(\)` is deprecated/u); - }); - - it('should throw if called from an invalid test case', () => { - assert.throws(() => { - ruleTester.run('use-get-comments', useGetCommentsRule, { - valid: [], - invalid: [ - { - code: '', - errors: [{}], - }, - ], - }); - }, /`SourceCode#getComments\(\)` is deprecated/u); - }); - }); - - describe('Subclassing', () => { - it('should allow subclasses to set the describe/it/itOnly statics and should correctly use those values', () => { - const assertionDescribe = assertEmitted( - ruleTesterTestEmitter, - 'custom describe', - 'this-is-a-rule-name', - ); - const assertionIt = assertEmitted( - ruleTesterTestEmitter, - 'custom it', - 'valid(code);', - ); - const assertionItOnly = assertEmitted( - ruleTesterTestEmitter, - 'custom itOnly', - 'validOnly(code);', - ); - - /** - * Subclass for testing - */ - class RuleTesterSubclass extends RuleTester {} - RuleTesterSubclass.describe = function (text, method) { - ruleTesterTestEmitter.emit('custom describe', text, method); - return method.call(this); - }; - RuleTesterSubclass.it = function (text, method) { - ruleTesterTestEmitter.emit('custom it', text, method); - return method.call(this); - }; - RuleTesterSubclass.itOnly = function (text, method) { - ruleTesterTestEmitter.emit('custom itOnly', text, method); - return method.call(this); - }; - - const ruleTesterSubclass = new RuleTesterSubclass(); - - ruleTesterSubclass.run( - 'this-is-a-rule-name', - require('./fixtures/no-var'), - { - valid: [ - 'valid(code);', - { - code: 'validOnly(code);', - only: true, - }, - ], - invalid: [], - }, - ); + ruleTesterSubclass.run("this-is-a-rule-name", require("./fixtures/no-var"), { + valid: [ + "valid(code);", + { + code: "validOnly(code);", + only: true + } + ], + invalid: [] + }); + + return Promise.all([ + assertionDescribe, + assertionIt, + assertionItOnly + ]); + }); - return Promise.all([assertionDescribe, assertionIt, assertionItOnly]); }); - }); + }); From 555c5b69d00e12d4c4b21698044764b08642edd1 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Tue, 7 May 2024 07:02:51 -0500 Subject: [PATCH 4/8] fix TS error --- packages/rule-tester/tests/RuleTester.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rule-tester/tests/RuleTester.test.ts b/packages/rule-tester/tests/RuleTester.test.ts index 2cfc3678ad58..20bc0c9cb4a7 100644 --- a/packages/rule-tester/tests/RuleTester.test.ts +++ b/packages/rule-tester/tests/RuleTester.test.ts @@ -112,6 +112,7 @@ runRuleForItemSpy.mockImplementation((_1, _2, testCase) => { output: testCase.code, afterAST: EMPTY_PROGRAM, beforeAST: EMPTY_PROGRAM, + config: { parser: '' }, }; }); From c9ef837c0a9bb2c6fe3bdff2236cf89d9adfae10 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Mon, 13 May 2024 07:22:55 -0500 Subject: [PATCH 5/8] fix test, it now fails --- .../tests/rules/switch-exhaustiveness-check.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts index b7919942e011..bdbcd589028b 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -1999,7 +1999,7 @@ switch (value) { switch (a) { case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } - case Enum['\\'a\\' \`b\` "c"']: { throw new Error('Not implemented yet: Enum[\\'\\\\'a\\\\' \`b\` "c"\\'] case') } + case Enum['\\'a\\' \`b\` "c"']: { throw new Error('Not implemented yet: Enum[\\'\\'a\\' \`b\` "c"\\'] case') } } `, }, From 4a1a67e5e6665de0b50a0481b4c85c02106d1530 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Mon, 13 May 2024 07:42:04 -0500 Subject: [PATCH 6/8] properly escape --- .../src/rules/switch-exhaustiveness-check.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 1ea0b7c78cb7..414536aa3508 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -217,6 +217,7 @@ export default createRule({ ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion missingBranchName! : checker.typeToString(missingBranchType); + let caseTestForErrorMessage = caseTest; if ( symbolName && @@ -224,18 +225,16 @@ export default createRule({ requiresQuoting(missingBranchName.toString(), compilerOptions.target) ) { const escapedBranchName = missingBranchName - .replace(/'/g, "\\'") - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r'); + .replaceAll("'", "\\'") + .replaceAll('\n', '\\n') + .replaceAll('\r', '\\r'); caseTest = `${symbolName}['${escapedBranchName}']`; + caseTestForErrorMessage = `${symbolName}[\\'${escapedBranchName}\\']`; } - const errorMessage = `Not implemented yet: ${caseTest} case`; - const escapedErrorMessage = errorMessage.replace(/'/g, "\\'"); - missingCases.push( - `case ${caseTest}: { throw new Error('${escapedErrorMessage}') }`, + `case ${caseTest}: { throw new Error('Not implemented yet: ${caseTestForErrorMessage} case') }`, ); } @@ -305,9 +304,7 @@ export default createRule({ context.report({ node: node.discriminant, messageId: 'switchIsNotExhaustive', - data: { - missingBranches: 'default', - }, + data: { missingBranches: 'default' }, suggest: [ { messageId: 'addMissingCases', From 8a8d6089fbe20c5b74e724ee5c28b3771b16beec Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Tue, 14 May 2024 07:16:56 -0500 Subject: [PATCH 7/8] change escaping --- .../eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 6 +++--- .../tests/rules/switch-exhaustiveness-check.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 414536aa3508..1452d7de6cdb 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -217,7 +217,6 @@ export default createRule({ ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion missingBranchName! : checker.typeToString(missingBranchType); - let caseTestForErrorMessage = caseTest; if ( symbolName && @@ -230,11 +229,12 @@ export default createRule({ .replaceAll('\r', '\\r'); caseTest = `${symbolName}['${escapedBranchName}']`; - caseTestForErrorMessage = `${symbolName}[\\'${escapedBranchName}\\']`; } missingCases.push( - `case ${caseTest}: { throw new Error('Not implemented yet: ${caseTestForErrorMessage} case') }`, + `case ${caseTest}: { throw new Error('Not implemented yet: ${caseTest + .replaceAll('\\', '\\\\') + .replaceAll("'", "\\'")} case') }`, ); } diff --git a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts index bdbcd589028b..ff4e2a4199db 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -1964,7 +1964,7 @@ switch (value) { switch (a) { case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } - case Enum['key-with\\n\\n new-line']: { throw new Error('Not implemented yet: Enum[\\'key-with\\n\\n new-line\\'] case') } + case Enum['key-with\\n\\n new-line']: { throw new Error('Not implemented yet: Enum[\\'key-with\\\\n\\\\n new-line\\'] case') } } `, }, @@ -1999,7 +1999,7 @@ switch (value) { switch (a) { case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } - case Enum['\\'a\\' \`b\` "c"']: { throw new Error('Not implemented yet: Enum[\\'\\'a\\' \`b\` "c"\\'] case') } + case Enum['\\'a\\' \`b\` "c"']: { throw new Error('Not implemented yet: Enum[\\'\\\\\\'a\\\\\\' \`b\` "c"\\'] case') } } `, }, From a83ef1fd5490d0ad4c7c024fa86256fa5ed9e7ca Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Tue, 14 May 2024 07:26:40 -0500 Subject: [PATCH 8/8] lint --- packages/rule-tester/src/RuleTester.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 5e94391e7231..b4911ef6febc 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -1060,7 +1060,7 @@ export class RuleTester extends TestFramework { !errorMessageInSuggestion, [ 'A fatal parsing error occurred in suggestion fix.', - `Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`, + `Error: ${errorMessageInSuggestion?.message}`, 'Suggestion output:', codeWithAppliedSuggestion, ].join('\n'),