diff --git a/lib/rule-tester/flat-rule-tester.js b/lib/rule-tester/flat-rule-tester.js index 01163c48e4d..c790dab69e7 100644 --- a/lib/rule-tester/flat-rule-tester.js +++ b/lib/rule-tester/flat-rule-tester.js @@ -853,6 +853,22 @@ class FlatRuleTester { const result = runRuleForItem(item); const messages = result.messages; + for (const message of messages) { + if (hasOwnProperty(message, "suggestions")) { + + /** @type {Map} */ + 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) { diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 626d9dcd3ce..bd46fcc807a 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -845,6 +845,22 @@ class RuleTester { const result = runRuleForItem(item); const messages = result.messages; + for (const message of messages) { + if (hasOwnProperty(message, "suggestions")) { + + /** @type {Map} */ + 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) { diff --git a/tests/fixtures/testers/rule-tester/suggestions.js b/tests/fixtures/testers/rule-tester/suggestions.js index ecedde04637..57cb84f1653 100644 --- a/tests/fixtures/testers/rule-tester/suggestions.js +++ b/tests/fixtures/testers/rule-tester/suggestions.js @@ -59,6 +59,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 { diff --git a/tests/lib/rule-tester/flat-rule-tester.js b/tests/lib/rule-tester/flat-rule-tester.js index c6d0730b3c6..fe97bd2c851 100644 --- a/tests/lib/rule-tester/flat-rule-tester.js +++ b/tests/lib/rule-tester/flat-rule-tester.js @@ -2313,6 +2313,39 @@ describe("FlatRuleTester", () => { }, /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/testers/rule-tester/suggestions").withoutHasSuggestionsProperty, { diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 23a748bc536..5af50dd7dea 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2187,6 +2187,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/testers/rule-tester/suggestions").withoutHasSuggestionsProperty, {