diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index f0ac7ca3dcac..324700fa264a 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -704,6 +704,23 @@ export class RuleTester extends TestFramework { const result = this.runRuleForItem(ruleName, rule, item); const messages = result.messages; + for (const message of messages) { + if (hasOwnProperty(message, 'suggestions')) { + const seenMessageIndices = new Map(); + + for (let i = 0; i < message.suggestions.length; i += 1) { + const suggestionMessage = message.suggestions[i].desc; + const previous = seenMessageIndices.get(suggestionMessage); + + assert.ok( + !seenMessageIndices.has(suggestionMessage), + `Suggestion message '${suggestionMessage}' reported from suggestion ${i} was previously reported by suggestion ${previous}. Suggestion messages should be unique within an error.`, + ); + seenMessageIndices.set(suggestionMessage, i); + } + } + } + if (typeof item.errors === 'number') { if (item.errors === 0) { assert.fail("Invalid cases must have 'error' value greater than 0"); 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 b1dabac0bb4c..8304dee78f95 100644 --- a/packages/rule-tester/tests/eslint-base/eslint-base.test.js +++ b/packages/rule-tester/tests/eslint-base/eslint-base.test.js @@ -2226,6 +2226,39 @@ describe("RuleTester", () => { }, /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, { diff --git a/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js b/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js index 4638ac2cacbf..d883167889e0 100644 --- a/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js +++ b/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js @@ -61,6 +61,98 @@ module.exports.withMessageIds = { } }; +module.exports.withDuplicateDescriptions = { + meta: { + hasSuggestions: true + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + message: "Avoid using identifiers name 'foo'.", + suggest: [{ + desc: "Rename 'foo' to 'bar'", + fix: fixer => fixer.replaceText(node, "bar") + }, { + desc: "Rename 'foo' to 'bar'", + fix: fixer => fixer.replaceText(node, "baz") + }] + }); + } + } + }; + } +}; + +module.exports.withDuplicateMessageIdsNoData = { + meta: { + messages: { + avoidFoo: "Avoid using identifiers named '{{ name }}'.", + renameFoo: "Rename identifier" + }, + hasSuggestions: true + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + messageId: "avoidFoo", + data: { + name: "foo" + }, + suggest: [{ + messageId: "renameFoo", + fix: fixer => fixer.replaceText(node, "bar") + }, { + messageId: "renameFoo", + fix: fixer => fixer.replaceText(node, "baz") + }] + }); + } + } + }; + } +}; + +module.exports.withDuplicateMessageIdsWithData = { + meta: { + messages: { + avoidFoo: "Avoid using identifiers named foo.", + renameFoo: "Rename identifier 'foo' to '{{ newName }}'" + }, + hasSuggestions: true + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + messageId: "avoidFoo", + suggest: [{ + messageId: "renameFoo", + data: { + newName: "bar" + }, + fix: fixer => fixer.replaceText(node, "bar") + }, { + messageId: "renameFoo", + data: { + newName: "bar" + }, + fix: fixer => fixer.replaceText(node, "baz") + }] + }); + } + } + }; + } +}; + module.exports.withoutHasSuggestionsProperty = { create(context) { return {