From 1e3529a6f1d7f99fb15ecfe15ed9f6ad5c4847f0 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Mon, 16 Oct 2023 11:07:58 +0200 Subject: [PATCH 01/24] feat!: rule tester require suggestion matchers --- lib/rule-tester/rule-tester.js | 1 + tests/lib/rule-tester/rule-tester.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index bff284ea8e6..5aa2c1325c4 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -991,6 +991,7 @@ class RuleTester { assert.strictEqual(message.endColumn, error.endColumn, `Error endColumn should be ${error.endColumn}`); } + assert.ok(!message.suggestions || hasOwnProperty(error, "suggestions"), `Error should have suggestions on error with message: "${message.message}`); if (hasOwnProperty(error, "suggestions")) { // Support asserting there are no suggestions diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 4d892c9fac2..29093c74ecb 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -1939,6 +1939,20 @@ describe("RuleTester", () => { }); describe("suggestions", () => { + it("should throw if suggestions are available but not specified", () => { + assert.throw(() => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [ + "var boo;" + ], + invalid: [{ + code: "var foo;", + errors: [{}] + }] + }); + }, "Error should have suggestions on error with message: \"Avoid using identifiers named 'foo'."); + }); + it("should pass with valid suggestions (tested using desc)", () => { ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { valid: [ From f9edbb66ddeee292537b7967d325d6cadddf5ee5 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Thu, 26 Oct 2023 17:24:44 +0200 Subject: [PATCH 02/24] feat!: rule tester require message or messageId --- lib/rule-tester/rule-tester.js | 7 +-- tests/lib/rule-tester/rule-tester.js | 66 +++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 5aa2c1325c4..f73c334ae31 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -964,13 +964,10 @@ class RuleTester { `Hydrated message "${rehydratedMessage}" does not match "${message.message}"` ); } + } else { + assert.fail("Error must have either a 'messageId' or 'message'."); } - assert.ok( - hasOwnProperty(error, "data") ? hasOwnProperty(error, "messageId") : true, - "Error must specify 'messageId' if 'data' is used." - ); - if (error.type) { assert.strictEqual(message.nodeType, error.type, `Error type should be ${error.type}, found ${message.nodeType}`); } diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 29093c74ecb..cfc67af7b2f 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -580,7 +580,7 @@ describe("RuleTester", () => { "bar = baz;" ], invalid: [ - { code: "var foo = bar; var baz = quux", errors: [{ type: "VariableDeclaration" }, null] } + { code: "var foo = bar; var baz = quux", errors: [{ message: "Bad var.", type: "VariableDeclaration" }, null] } ] }); }, /Error should be a string, object, or RegExp/u); @@ -1020,14 +1020,28 @@ describe("RuleTester", () => { }, /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/testers/rule-tester/no-eval"), { - valid: ["Eval(foo)"], - invalid: [{ - code: "eval(foo)", - errors: [{}] - }] - }); + it("should throw an error if an error object has no properties", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("../../fixtures/testers/rule-tester/no-eval"), { + valid: ["Eval(foo)"], + invalid: [{ + code: "eval(foo)", + errors: [{}] + }] + }); + }, "Error must have either a 'messageId' or 'message'."); + }); + + it("should throw an error if an error has a property besides message or messageId", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("../../fixtures/testers/rule-tester/no-eval"), { + valid: ["Eval(foo)"], + invalid: [{ + code: "eval(foo)", + errors: [{ line: 1 }] + }] + }); + }, "Error must have either a 'messageId' or 'message'."); }); it("should pass-through the globals config of valid tests to the to rule", () => { @@ -1258,7 +1272,7 @@ describe("RuleTester", () => { languageOptions: { parser: esprima }, - errors: [{ line: 1 }] + errors: [{ message: "eval sucks.", line: 1 }] } ] }); @@ -1892,7 +1906,7 @@ describe("RuleTester", () => { valid: [], invalid: [{ code: "foo", errors: [{ data: "something" }] }] }); - }, "Error must specify 'messageId' if 'data' is used."); + }, "Error must have either a 'messageId' or 'message'."); }); // fixable rules with or without `meta` property @@ -1947,7 +1961,7 @@ describe("RuleTester", () => { ], invalid: [{ code: "var foo;", - errors: [{}] + errors: [{ message: "Avoid using identifiers named 'foo'." }] }] }); }, "Error should have suggestions on error with message: \"Avoid using identifiers named 'foo'."); @@ -1961,6 +1975,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var bar;" @@ -1977,11 +1992,13 @@ describe("RuleTester", () => { { code: "function foo() {\n var foo = 1;\n}", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "function bar() {\n var foo = 1;\n}" }] }, { + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "function foo() {\n var bar = 1;\n}" @@ -1998,6 +2015,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2016,6 +2034,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2034,6 +2053,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", messageId: "renameFoo", @@ -2054,6 +2074,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var bar;" @@ -2072,6 +2093,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "bar" }, @@ -2093,6 +2115,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{}, {}] }] }] @@ -2106,6 +2129,7 @@ describe("RuleTester", () => { invalid: [{ code: "eval('var foo');", errors: [{ + message: "eval sucks.", suggestions }] }] @@ -2121,6 +2145,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions }] }] @@ -2136,6 +2161,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Bad var.", suggestions: [{ messageId: "this-does-not-exist" }] @@ -2152,6 +2178,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var bar;" @@ -2213,6 +2240,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "not right", output: "var baz;" @@ -2230,6 +2258,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", messageId: "renameFoo", @@ -2252,6 +2281,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "unused", output: "var bar;" @@ -2272,6 +2302,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", messageId: "renameFoo", @@ -2294,6 +2325,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2311,6 +2343,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2331,6 +2364,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "car" }, @@ -2353,6 +2387,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "bar" }, @@ -2375,6 +2410,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", messageId: "renameFoo", @@ -2398,6 +2434,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "bar" }, @@ -2419,6 +2456,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var baz;" @@ -2436,6 +2474,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [null] }] }] @@ -2448,6 +2487,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [ { messageId: "renameFoo", @@ -2470,6 +2510,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ message: "Rename identifier 'foo' to 'bar'" }] @@ -2486,6 +2527,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", output: "var bar;" From e85f3ae951560177aaaac5b6746e7652adeba36e Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Thu, 2 Nov 2023 15:38:38 +0100 Subject: [PATCH 03/24] feat!: rule tester require output for suggestions --- lib/rule-tester/rule-tester.js | 23 +++++++++-------- tests/lib/rule-tester/rule-tester.js | 37 ++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index f73c334ae31..2e455af935e 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -1058,22 +1058,21 @@ class RuleTester { ); } - if (hasOwnProperty(expectedSuggestion, "output")) { - const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output; + assert.ok(hasOwnProperty(expectedSuggestion, "output"), `${suggestionPrefix} The "output" property is required.`); + const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output; - // Verify if suggestion fix makes a syntax error or not. - const errorMessageInSuggestion = + // Verify if suggestion fix makes a syntax error or not. + const errorMessageInSuggestion = linter.verify(codeWithAppliedSuggestion, result.configs, 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(!errorMessageInSuggestion, [ + "A fatal parsing error occurred in suggestion fix.", + `Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`, + "Suggestion output:", + codeWithAppliedSuggestion + ].join("\n")); - assert.strictEqual(codeWithAppliedSuggestion, expectedSuggestion.output, `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`); - } + assert.strictEqual(codeWithAppliedSuggestion, expectedSuggestion.output, `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`); }); } } diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index cfc67af7b2f..ec86f43ae69 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2109,17 +2109,34 @@ describe("RuleTester", () => { }); - it("should pass when tested using empty suggestion test objects if the array length is correct", () => { - ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - messageId: "avoidFoo", - suggestions: [{}, {}] + it("should fail when tested using empty suggestion test objects even if the array length is correct", () => { + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{}, {}] + }] }] - }] - }); + }); + }, 'Error Suggestion at index 0 : The "output" property is required.'); + }); + + it("should fail when tested using non-empty suggestion test objects without an output property", () => { + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ messageId: "renameFoo" }, {}] + }] + }] + }); + }, 'Error Suggestion at index 0 : The "output" property is required.'); }); it("should support explicitly expecting no suggestions", () => { From 9b3fa60689c5eb19f7bf8b7d6bebf421c5be6cfb Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Thu, 2 Nov 2023 16:12:21 +0100 Subject: [PATCH 04/24] feat!: rule tester only allow desc or messageId in suggestion matchers --- lib/rule-tester/rule-tester.js | 17 +++-- tests/lib/rule-tester/rule-tester.js | 96 +++++++++++----------------- 2 files changed, 47 insertions(+), 66 deletions(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 2e455af935e..529a5b05892 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -1020,14 +1020,16 @@ class RuleTester { !hasOwnProperty(expectedSuggestion, "data"), `${suggestionPrefix} Test should not specify both 'desc' and 'data'.` ); + assert.ok( + !hasOwnProperty(expectedSuggestion, "messageId"), + `${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.` + ); assert.strictEqual( actualSuggestion.desc, expectedSuggestion.desc, `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.` ); - } - - if (hasOwnProperty(expectedSuggestion, "messageId")) { + } else if (hasOwnProperty(expectedSuggestion, "messageId")) { assert.ok( ruleHasMetaMessages, `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.` @@ -1051,11 +1053,14 @@ class RuleTester { `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".` ); } - } else { - assert.ok( - !hasOwnProperty(expectedSuggestion, "data"), + } else if (hasOwnProperty(expectedSuggestion, "data")) { + assert.fail( `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.` ); + } else { + assert.fail( + `${suggestionPrefix} Test must specify either 'messageId' or 'desc'.` + ); } assert.ok(hasOwnProperty(expectedSuggestion, "output"), `${suggestionPrefix} The "output" property is required.`); diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index ec86f43ae69..c2d8268a382 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2047,25 +2047,27 @@ describe("RuleTester", () => { }); }); - it("should pass with valid suggestions (tested using both desc and messageIds for the same suggestion)", () => { - ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - messageId: "avoidFoo", - 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 fail with valid suggestions when testing using both desc and messageIds for the same suggestion", () => { + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + messageId: "renameFoo", + output: "var bar;" + }, { + desc: "Rename identifier 'foo' to 'baz'", + messageId: "renameFoo", + output: "var baz;" + }] }] }] - }] - }); + }); + }, "Error Suggestion at index 0 : Test should not specify both 'desc' and 'messageId'."); }); it("should pass with valid suggestions (tested using only desc on a rule that utilizes meta.messages)", () => { @@ -2121,7 +2123,7 @@ describe("RuleTester", () => { }] }] }); - }, 'Error Suggestion at index 0 : The "output" property is required.'); + }, "Error Suggestion at index 0 : Test must specify either 'messageId' or 'desc'"); }); it("should fail when tested using non-empty suggestion test objects without an output property", () => { @@ -2268,27 +2270,24 @@ describe("RuleTester", () => { }, "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/testers/rule-tester/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - messageId: "avoidFoo", - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename id 'foo' to 'baz'", - messageId: "renameFoo", - output: "var baz;" - }] + + it("should pass when different suggestion matchers use desc and messageId", () => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var bar;" + }, { + 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", () => { @@ -2312,29 +2311,6 @@ describe("RuleTester", () => { }, "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/testers/rule-tester/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - messageId: "avoidFoo", - 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/testers/rule-tester/suggestions").basic, { From 0f90cbe28e07672181d1625e7da5ca38e3bb9990 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Thu, 2 Nov 2023 16:19:04 +0100 Subject: [PATCH 05/24] feat!: rule tester require suggestion output differs from original source code --- lib/rule-tester/rule-tester.js | 1 + .../testers/rule-tester/suggestions.js | 20 +++++++++++++++++++ tests/lib/rule-tester/rule-tester.js | 18 +++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 529a5b05892..12a2db6f32d 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -1078,6 +1078,7 @@ class RuleTester { ].join("\n")); assert.strictEqual(codeWithAppliedSuggestion, expectedSuggestion.output, `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`); + assert.notStrictEqual(expectedSuggestion.output, item.code, `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`); }); } } diff --git a/tests/fixtures/testers/rule-tester/suggestions.js b/tests/fixtures/testers/rule-tester/suggestions.js index 57cb84f1653..b46258fa04e 100644 --- a/tests/fixtures/testers/rule-tester/suggestions.js +++ b/tests/fixtures/testers/rule-tester/suggestions.js @@ -164,3 +164,23 @@ module.exports.withoutHasSuggestionsProperty = { }; } }; + +module.exports.withFixerWithoutChanges = { + meta: { hasSuggestions: true }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + message: "Avoid using identifiers named 'foo'.", + suggest: [{ + desc: "Rename identifier 'foo' to 'bar'", + fix: fixer => fixer.replaceText(node, 'foo') + }] + }); + } + } + }; + } +}; diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index c2d8268a382..2fc9f7efbb5 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2460,6 +2460,24 @@ describe("RuleTester", () => { }, "Expected the applied suggestion fix to match the test suggestion output"); }); + it("should throw if the resulting suggestion output is the same as the original source code", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").withFixerWithoutChanges, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var foo;" + }] + }] + }] + }); + }, "The output of a suggestion should differ from the original source code for suggestion at index: 0 on error with message: \"Avoid using identifiers named 'foo'.\""); + }); + it("should fail when specified suggestion isn't an object", () => { assert.throws(() => { ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { From a618f0c97782ad45d46bfff808bc569761ceb498 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Mon, 13 Nov 2023 14:08:16 +0100 Subject: [PATCH 06/24] feat!: rule tester typecheck only and filename test case properties --- lib/rule-tester/rule-tester.js | 6 ++++- tests/lib/rule-tester/rule-tester.js | 37 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 12a2db6f32d..42ec5d39c0d 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -631,7 +631,11 @@ class RuleTester { configs.push(itemConfig); } - if (item.filename) { + if (hasOwnProperty(item, "only")) { + assert.ok(typeof item.only === "boolean", "Optional test case property 'only' must be a boolean"); + } + if (hasOwnProperty(item, "filename")) { + assert.ok(typeof item.filename === "string", "Optional test case property 'filename' must be a string"); filename = item.filename; } diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 2fc9f7efbb5..d3284218b27 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2835,6 +2835,43 @@ describe("RuleTester", () => { }, /A fatal parsing error occurred in autofix.\nError: .+\nAutofix output:\n.+/u); }); + describe("type checking", () => { + it('should throw if "only" property is not a boolean', () => { + + // "only" has to be falsy as itOnly is not mocked for all test cases + assert.throws(() => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [{ code: "foo", only: "" }], + invalid: [] + }); + }, /Optional test case property 'only' must be a boolean/u); + + assert.throws(() => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [], + invalid: [{ code: "foo", only: 0, errors: 1 }] + }); + }, /Optional test case property 'only' must be a boolean/u); + }); + + it('should throw if "filename" property is not a string', () => { + assert.throws(() => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [{ code: "foo", filename: false }], + invalid: [] + + }); + }, /Optional test case property 'filename' must be a string/u); + + assert.throws(() => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/no-var"), { + valid: ["foo"], + invalid: [{ code: "foo", errors: 1, filename: 0 }] + }); + }, /Optional test case property 'filename' must be a string/u); + }); + }); + describe("sanitize test cases", () => { let originalRuleTesterIt; let spyRuleTesterIt; From 603bccaa287b2d3a88e76f94cf10b1cd63c95322 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Mon, 4 Dec 2023 13:17:17 +0100 Subject: [PATCH 07/24] feat!: rule tester check whether code and output is equal --- lib/rule-tester/rule-tester.js | 1 + tests/lib/rule-tester/rule-tester.js | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 42ec5d39c0d..019d3b22252 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -1103,6 +1103,7 @@ class RuleTester { ); } else { assert.strictEqual(result.output, item.output, "Output is incorrect."); + assert.notStrictEqual(item.code, item.output, "Use 'output: null' if the rule does not fix this case instead of copying the code."); } } else { assert.strictEqual( diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index d3284218b27..b50bc33dfda 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -785,6 +785,17 @@ describe("RuleTester", () => { }, /Expected no autofixes to be suggested/u); }); + it("should throw an error when the expected output is not null and the output does not differ from the code", () => { + assert.throws(() => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-eval"), { + valid: [], + invalid: [ + { code: "eval('')", output: "eval('')", errors: 1 } + ] + }); + }, /Use 'output: null' if the rule does not fix this case instead of copying the code\./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/testers/rule-tester/no-var"), { From 8db902f473ed4866f793021d1bd4fe370e58f306 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Mon, 4 Dec 2023 13:25:55 +0100 Subject: [PATCH 08/24] docs: update rule tester documentation to reflect additional assertions --- docs/src/integrate/nodejs-api.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/src/integrate/nodejs-api.md b/docs/src/integrate/nodejs-api.md index ebe08109269..ae72368955c 100644 --- a/docs/src/integrate/nodejs-api.md +++ b/docs/src/integrate/nodejs-api.md @@ -735,19 +735,19 @@ A test case is an object with the following properties: In addition to the properties above, invalid test cases can also have the following properties: -* `errors` (number or array, required): Asserts some properties of the errors that the rule is expected to produce when run on this code. If this is a number, asserts the number of errors produced. Otherwise, this should be a list of objects, each containing information about a single reported error. The following properties can be used for an error (all are optional): - * `message` (string/regexp): The message for the error - * `messageId` (string): The Id for the error. See [testing errors with messageId](#testing-errors-with-messageid) for details - * `data` (object): Placeholder data which can be used in combination with `messageId` +* `errors` (number or array, required): Asserts some properties of the errors that the rule is expected to produce when run on this code. If this is a number, asserts the number of errors produced. Otherwise, this should be a list of objects, each containing information about a single reported error. The following properties can be used for an error (all are optional unless otherwise noted): + * `message` (string/regexp): The message for the error. Must provide this or `messageId` + * `messageId` (string): The Id for the error. Must provide this or `message`. See [testing errors with messageId](#testing-errors-with-messageid) for details + * `data` (object): Placeholder data which can be used in combination with `messageId`. Required if `messageId` is specified and its message uses placeholders * `type` (string): The type of the reported AST node * `line` (number): The 1-based line number of the reported location * `column` (number): The 1-based column number of the reported location * `endLine` (number): The 1-based line number of the end of the reported location * `endColumn` (number): The 1-based column number of the end of the reported location - * `suggestions` (array): An array of objects with suggestion details to check. See [Testing Suggestions](#testing-suggestions) for details + * `suggestions` (array): An array of objects with suggestion details to check. Required if the rule produces suggestions. See [Testing Suggestions](#testing-suggestions) for details If a string is provided as an error instead of an object, the string is used to assert the `message` of the error. -* `output` (string, required if the rule fixes code): Asserts the output that will be produced when using this rule for a single pass of autofixing (e.g. with the `--fix` command line flag). If this is `null`, asserts that none of the reported problems suggest autofixes. +* `output` (string, required if the rule fixes code): Asserts the output that will be produced when using this rule for a single pass of autofixing (e.g. with the `--fix` command line flag). If this is `null`, asserts that none of the reported problems suggest autofixes or the fixer did not create a fix. Any additional properties of a test case will be passed directly to the linter as config options. For example, a test case can have a `languageOptions` property to configure parser behavior: @@ -784,12 +784,12 @@ Please note that `data` in a test case does not assert `data` passed to `context ### Testing Suggestions -Suggestions can be tested by defining a `suggestions` key on an errors object. The options to check for the suggestions are the following (all are optional): +Suggestions can be tested by defining a `suggestions` key on an errors object. The options to check for the suggestions are the following: -* `desc` (string): The suggestion `desc` value -* `messageId` (string): The suggestion `messageId` value for suggestions that use `messageId`s -* `data` (object): Placeholder data which can be used in combination with `messageId` -* `output` (string): A code string representing the result of applying the suggestion fix to the input code +* `desc` (string): The suggestion `desc` value. Must provide this or `messageId` +* `messageId` (string): The suggestion `messageId` value for suggestions that use `messageId`s. Must provide this or `desc` +* `data` (object): Placeholder data which can be used in combination with `messageId`. Required if `messageId` is specified and its message uses placeholders +* `output` (string, required): A code string representing the result of applying the suggestion fix to the input code Example: From 285a5752513e8b6b02570eaea3e919efda5acaf4 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Mon, 4 Dec 2023 15:14:21 +0100 Subject: [PATCH 09/24] fix: fix invalid test cases of builtin rules --- tests/lib/rules/array-callback-return.js | 19 +++++++++++-------- tests/lib/rules/max-params.js | 1 + tests/lib/rules/no-object-constructor.js | 6 ------ tests/lib/rules/no-useless-return.js | 2 +- .../lib/rules/one-var-declaration-per-line.js | 4 ++-- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/tests/lib/rules/array-callback-return.js b/tests/lib/rules/array-callback-return.js index 18057c676b7..a34acd33c53 100644 --- a/tests/lib/rules/array-callback-return.js +++ b/tests/lib/rules/array-callback-return.js @@ -204,7 +204,7 @@ ruleTester.run("array-callback-return", rule, { { code: "foo.every(cb || function() {})", options: allowImplicitOptions, errors: ["Array.prototype.every() expects a return value from function."] }, { code: "[\"foo\",\"bar\"].sort(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.sort" } }] }, { code: "[\"foo\",\"bar\"].toSorted(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.toSorted" } }] }, - { code: "foo.forEach(x => x)", options: allowImplicitCheckForEach, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(x => x)", options: allowImplicitCheckForEach, languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach(x => {x})" }] }] }, { code: "foo.forEach(function(x) { if (a == b) {return x;}})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, { code: "foo.forEach(function bar(x) { return x;})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function 'bar'", arrayMethodName: "Array.prototype.forEach" } }] }, @@ -282,10 +282,10 @@ ruleTester.run("array-callback-return", rule, { { code: "foo.filter(function foo() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, { code: "foo.filter(function foo() { return; })", options: checkForEachOptions, errors: [{ messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, { code: "foo.every(cb || function() {})", options: checkForEachOptions, errors: ["Array.prototype.every() expects a return value from function."] }, - { code: "foo.forEach((x) => void x)", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, - { code: "foo.forEach((x) => void bar(x))", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, - { code: "foo.forEach((x) => { return void bar(x); })", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, - { code: "foo.forEach((x) => { if (a === b) { return void a; } bar(x) })", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach((x) => void x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((x) => {void x})" }] }] }, + { code: "foo.forEach((x) => void bar(x))", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((x) => {void bar(x)})" }] }] }, + { code: "foo.forEach((x) => { return void bar(x); })", options: checkForEachOptions, languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach((x) => { if (a === b) { return void a; } bar(x) })", options: checkForEachOptions, languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, // options: { checkForEach: true, allowVoid: true } @@ -490,7 +490,8 @@ ruleTester.run("array-callback-return", rule, { line: 1, column: 17, endLine: 1, - endColumn: 19 + endColumn: 19, + suggestions: [{ messageId: "wrapBraces", output: "foo.forEach(bar => {bar})" }] }] }, { @@ -504,7 +505,8 @@ ruleTester.run("array-callback-return", rule, { line: 1, column: 41, endLine: 1, - endColumn: 43 + endColumn: 43, + suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((function () { return (bar) => {bar}; })())" }] }] }, { @@ -518,7 +520,8 @@ ruleTester.run("array-callback-return", rule, { line: 2, column: 13, endLine: 2, - endColumn: 15 + endColumn: 15, + suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((() => {\n return bar => {bar}; })())" }] }] }, { diff --git a/tests/lib/rules/max-params.js b/tests/lib/rules/max-params.js index 257869febbe..10ae32119e0 100644 --- a/tests/lib/rules/max-params.js +++ b/tests/lib/rules/max-params.js @@ -118,6 +118,7 @@ ruleTester.run("max-params", rule, { }`, options: [{ max: 2 }], errors: [{ + messageId: "exceed", line: 1, column: 1, endLine: 1, diff --git a/tests/lib/rules/no-object-constructor.js b/tests/lib/rules/no-object-constructor.js index f8ff2d76c27..a0133c3d0e2 100644 --- a/tests/lib/rules/no-object-constructor.js +++ b/tests/lib/rules/no-object-constructor.js @@ -41,7 +41,6 @@ ruleTester.run("no-object-constructor", rule, { messageId: "preferLiteral", type: "NewExpression", suggestions: [{ - desc: "Replace with '({})'.", messageId: "useLiteral", output: "({})" }] @@ -53,7 +52,6 @@ ruleTester.run("no-object-constructor", rule, { messageId: "preferLiteral", type: "CallExpression", suggestions: [{ - desc: "Replace with '({})'.", messageId: "useLiteral", output: "({})" }] @@ -65,7 +63,6 @@ ruleTester.run("no-object-constructor", rule, { messageId: "preferLiteral", type: "CallExpression", suggestions: [{ - desc: "Replace with '({})'.", messageId: "useLiteral", output: "const fn = () => ({});" }] @@ -77,7 +74,6 @@ ruleTester.run("no-object-constructor", rule, { messageId: "preferLiteral", type: "CallExpression", suggestions: [{ - desc: "Replace with '({})'.", messageId: "useLiteral", output: "({}) instanceof Object;" }] @@ -89,7 +85,6 @@ ruleTester.run("no-object-constructor", rule, { messageId: "preferLiteral", type: "CallExpression", suggestions: [{ - desc: "Replace with '{}'.", messageId: "useLiteral", output: "const obj = {};" }] @@ -101,7 +96,6 @@ ruleTester.run("no-object-constructor", rule, { messageId: "preferLiteral", type: "NewExpression", suggestions: [{ - desc: "Replace with '{}'.", messageId: "useLiteral", output: "({} instanceof Object);" }] diff --git a/tests/lib/rules/no-useless-return.js b/tests/lib/rules/no-useless-return.js index d599cfe3067..9dfd4a8879d 100644 --- a/tests/lib/rules/no-useless-return.js +++ b/tests/lib/rules/no-useless-return.js @@ -231,7 +231,7 @@ ruleTester.run("no-useless-return", rule, { }, { code: "function foo() { if (foo) return; }", - output: "function foo() { if (foo) return; }" + output: null }, { code: "function foo() { bar(); return/**/; }", diff --git a/tests/lib/rules/one-var-declaration-per-line.js b/tests/lib/rules/one-var-declaration-per-line.js index fb4f8e70c58..98969f78fb6 100644 --- a/tests/lib/rules/one-var-declaration-per-line.js +++ b/tests/lib/rules/one-var-declaration-per-line.js @@ -68,7 +68,7 @@ ruleTester.run("one-var-declaration-per-line", rule, { ], invalid: [ - { code: "var foo, bar;", output: "var foo, \nbar;", options: ["always"], errors: [{ line: 1, column: 10, endLine: 1, endColumn: 13 }] }, + { code: "var foo, bar;", output: "var foo, \nbar;", options: ["always"], errors: [{ messageId: "expectVarOnNewline", line: 1, column: 10, endLine: 1, endColumn: 13 }] }, { code: "var a, b;", output: "var a, \nb;", options: ["always"], errors: [errorAt(1, 8)] }, { code: "let a, b;", output: "let a, \nb;", options: ["always"], languageOptions: { ecmaVersion: 6 }, errors: [errorAt(1, 8)] }, { code: "var a, b = 0;", output: "var a, \nb = 0;", options: ["always"], errors: [errorAt(1, 8)] }, @@ -77,7 +77,7 @@ ruleTester.run("one-var-declaration-per-line", rule, { { code: "let a, b = 0;", output: "let a, \nb = 0;", options: ["always"], languageOptions: { ecmaVersion: 6 }, errors: [errorAt(1, 8)] }, { code: "const a = 0, b = 0;", output: "const a = 0, \nb = 0;", options: ["always"], languageOptions: { ecmaVersion: 6 }, errors: [errorAt(1, 14)] }, - { code: "var foo, bar, baz = 0;", output: "var foo, bar, \nbaz = 0;", options: ["initializations"], errors: [{ line: 1, column: 15, endLine: 1, endColumn: 22 }] }, + { code: "var foo, bar, baz = 0;", output: "var foo, bar, \nbaz = 0;", options: ["initializations"], errors: [{ messageId: "expectVarOnNewline", line: 1, column: 15, endLine: 1, endColumn: 22 }] }, { code: "var a, b, c = 0;", output: "var a, b, \nc = 0;", options: ["initializations"], errors: [errorAt(1, 11)] }, { code: "var a, b,\nc = 0, d;", output: "var a, b,\nc = 0, \nd;", options: ["initializations"], errors: [errorAt(2, 8)] }, { code: "var a, b,\nc = 0, d = 0;", output: "var a, b,\nc = 0, \nd = 0;", options: ["initializations"], errors: [errorAt(2, 8)] }, From 17df6d08f3bd1c05e8f4a64d061444a5df6e6c5b Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Mon, 4 Dec 2023 15:14:57 +0100 Subject: [PATCH 10/24] fix: remove unnecessary space for the suggestion prefix --- lib/rule-tester/rule-tester.js | 2 +- tests/lib/rule-tester/rule-tester.js | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 019d3b22252..f7196f54d23 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -1017,7 +1017,7 @@ class RuleTester { }); const actualSuggestion = message.suggestions[index]; - const suggestionPrefix = `Error Suggestion at index ${index} :`; + const suggestionPrefix = `Error Suggestion at index ${index}:`; if (hasOwnProperty(expectedSuggestion, "desc")) { assert.ok( diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index b50bc33dfda..356c2d50dde 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2078,7 +2078,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : Test should not specify both 'desc' and 'messageId'."); + }, "Error Suggestion at index 0: Test should not specify both 'desc' and 'messageId'."); }); it("should pass with valid suggestions (tested using only desc on a rule that utilizes meta.messages)", () => { @@ -2134,7 +2134,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : Test must specify either 'messageId' or 'desc'"); + }, "Error Suggestion at index 0: Test must specify either 'messageId' or 'desc'"); }); it("should fail when tested using non-empty suggestion test objects without an output property", () => { @@ -2149,7 +2149,7 @@ describe("RuleTester", () => { }] }] }); - }, 'Error Suggestion at index 0 : The "output" property is required.'); + }, 'Error Suggestion at index 0: The "output" property is required.'); }); it("should support explicitly expecting no suggestions", () => { @@ -2278,7 +2278,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : desc should be \"not right\" but got \"Rename identifier 'foo' to 'bar'\" instead."); + }, "Error Suggestion at index 0: desc should be \"not right\" but got \"Rename identifier 'foo' to 'bar'\" instead."); }); @@ -2319,7 +2319,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : messageId should be 'unused' but got 'renameFoo' instead."); + }, "Error Suggestion at index 0: messageId should be 'unused' but got 'renameFoo' instead."); }); it("should throw if test specifies messageId for a rule that doesn't have meta.messages", () => { @@ -2337,7 +2337,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : Test can not use 'messageId' if rule under test doesn't define 'meta.messages'."); + }, "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", () => { @@ -2358,7 +2358,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 1 : Test has invalid messageId 'removeFoo', the rule under test allows only one of ['avoidFoo', 'unused', 'renameFoo']."); + }, "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)", () => { @@ -2381,7 +2381,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : Hydrated test desc \"Rename identifier 'foo' to 'car'\" does not match received desc \"Rename identifier 'foo' to 'bar'\"."); + }, "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)", () => { @@ -2404,7 +2404,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 1 : Hydrated test desc \"Rename identifier 'foo' to '{{ newName }}'\" does not match received desc \"Rename identifier 'foo' to '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", () => { @@ -2428,7 +2428,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : Test should not specify both 'desc' and 'data'."); + }, "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", () => { @@ -2450,7 +2450,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 1 : Test must specify 'messageId' if 'data' is used."); + }, "Error Suggestion at index 1: Test must specify 'messageId' if 'data' is used."); }); it("should throw if the resulting suggestion output doesn't match", () => { From dde1be3f278087d954627541f8bf070f64baf26c Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Fri, 12 Jan 2024 22:21:00 +0100 Subject: [PATCH 11/24] chore: fixing merge diversions --- tests/lib/rule-tester/rule-tester.js | 1 + tests/lib/rules/array-callback-return.js | 4 ++-- tests/lib/rules/no-array-constructor.js | 2 -- tests/lib/rules/no-object-constructor.js | 2 -- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 356c2d50dde..b3327b83f42 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2252,6 +2252,7 @@ describe("RuleTester", () => { invalid: [{ code: "one()", errors: [{ + message: "make a syntax error", suggestions: [{ desc: "make a syntax error", output: "one two()" diff --git a/tests/lib/rules/array-callback-return.js b/tests/lib/rules/array-callback-return.js index a34acd33c53..390acca025e 100644 --- a/tests/lib/rules/array-callback-return.js +++ b/tests/lib/rules/array-callback-return.js @@ -282,8 +282,8 @@ ruleTester.run("array-callback-return", rule, { { code: "foo.filter(function foo() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, { code: "foo.filter(function foo() { return; })", options: checkForEachOptions, errors: [{ messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, { code: "foo.every(cb || function() {})", options: checkForEachOptions, errors: ["Array.prototype.every() expects a return value from function."] }, - { code: "foo.forEach((x) => void x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((x) => {void x})" }] }] }, - { code: "foo.forEach((x) => void bar(x))", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((x) => {void bar(x)})" }] }] }, + { code: "foo.forEach((x) => void x)", options: checkForEachOptions, languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((x) => {void x})" }] }] }, + { code: "foo.forEach((x) => void bar(x))", options: checkForEachOptions, languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((x) => {void bar(x)})" }] }] }, { code: "foo.forEach((x) => { return void bar(x); })", options: checkForEachOptions, languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, { code: "foo.forEach((x) => { if (a === b) { return void a; } bar(x) })", options: checkForEachOptions, languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, diff --git a/tests/lib/rules/no-array-constructor.js b/tests/lib/rules/no-array-constructor.js index c429ff1b450..04027d8b9cd 100644 --- a/tests/lib/rules/no-array-constructor.js +++ b/tests/lib/rules/no-array-constructor.js @@ -238,7 +238,6 @@ ruleTester.run("no-array-constructor", rule, { errors: [{ messageId: "preferLiteral", suggestions: [{ - desc: "Replace with an array literal, add preceding semicolon.", messageId: "useLiteralAfterSemicolon", output: props.code.replace(/(new )?Array\((?.*?)\)/su, ";[$]") }] @@ -405,7 +404,6 @@ ruleTester.run("no-array-constructor", rule, { errors: [{ messageId: "preferLiteral", suggestions: [{ - desc: "Replace with an array literal.", messageId: "useLiteral", output: props.code.replace(/(new )?Array\((?.*?)\)/su, "[$]") }] diff --git a/tests/lib/rules/no-object-constructor.js b/tests/lib/rules/no-object-constructor.js index a0133c3d0e2..5b94a12ffea 100644 --- a/tests/lib/rules/no-object-constructor.js +++ b/tests/lib/rules/no-object-constructor.js @@ -184,7 +184,6 @@ ruleTester.run("no-object-constructor", rule, { errors: [{ messageId: "preferLiteral", suggestions: [{ - desc: "Replace with '({})', add preceding semicolon.", messageId: "useLiteralAfterSemicolon", output: props.code.replace(/(new )?Object\(\)/u, ";({})") }] @@ -351,7 +350,6 @@ ruleTester.run("no-object-constructor", rule, { errors: [{ messageId: "preferLiteral", suggestions: [{ - desc: "Replace with '({})'.", messageId: "useLiteral", output: props.code.replace(/(new )?Object\(\)/u, "({})") }] From cc23f0f0c7c5633aff210b1b0ffdff755584e259 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Sat, 20 Jan 2024 20:25:12 +0100 Subject: [PATCH 12/24] fix: remove incorrect claim that data is required if messageId with placeholders is used --- docs/src/integrate/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/integrate/nodejs-api.md b/docs/src/integrate/nodejs-api.md index ae72368955c..76570c1ea56 100644 --- a/docs/src/integrate/nodejs-api.md +++ b/docs/src/integrate/nodejs-api.md @@ -738,7 +738,7 @@ In addition to the properties above, invalid test cases can also have the follow * `errors` (number or array, required): Asserts some properties of the errors that the rule is expected to produce when run on this code. If this is a number, asserts the number of errors produced. Otherwise, this should be a list of objects, each containing information about a single reported error. The following properties can be used for an error (all are optional unless otherwise noted): * `message` (string/regexp): The message for the error. Must provide this or `messageId` * `messageId` (string): The Id for the error. Must provide this or `message`. See [testing errors with messageId](#testing-errors-with-messageid) for details - * `data` (object): Placeholder data which can be used in combination with `messageId`. Required if `messageId` is specified and its message uses placeholders + * `data` (object): Placeholder data which can be used in combination with `messageId` * `type` (string): The type of the reported AST node * `line` (number): The 1-based line number of the reported location * `column` (number): The 1-based column number of the reported location From ebafff577ca0243a62ad5c92c1f34fd1827cdc06 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Sat, 20 Jan 2024 20:27:10 +0100 Subject: [PATCH 13/24] chore: remove potentially confusing differentation between missing and failing fixer --- docs/src/integrate/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/integrate/nodejs-api.md b/docs/src/integrate/nodejs-api.md index 76570c1ea56..181d93f3d98 100644 --- a/docs/src/integrate/nodejs-api.md +++ b/docs/src/integrate/nodejs-api.md @@ -747,7 +747,7 @@ In addition to the properties above, invalid test cases can also have the follow * `suggestions` (array): An array of objects with suggestion details to check. Required if the rule produces suggestions. See [Testing Suggestions](#testing-suggestions) for details If a string is provided as an error instead of an object, the string is used to assert the `message` of the error. -* `output` (string, required if the rule fixes code): Asserts the output that will be produced when using this rule for a single pass of autofixing (e.g. with the `--fix` command line flag). If this is `null`, asserts that none of the reported problems suggest autofixes or the fixer did not create a fix. +* `output` (string, required if the rule fixes code): Asserts the output that will be produced when using this rule for a single pass of autofixing (e.g. with the `--fix` command line flag). If this is `null` or omitted, asserts that none of the reported problems suggest autofixes. Any additional properties of a test case will be passed directly to the linter as config options. For example, a test case can have a `languageOptions` property to configure parser behavior: From 73c4363a42329cb4026c16b518087eefadbfa385 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Sat, 20 Jan 2024 20:29:44 +0100 Subject: [PATCH 14/24] fix: better explanation for missing suggestions --- lib/rule-tester/rule-tester.js | 2 +- tests/lib/rule-tester/rule-tester.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index f7196f54d23..7be6d42e359 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -992,7 +992,7 @@ class RuleTester { assert.strictEqual(message.endColumn, error.endColumn, `Error endColumn should be ${error.endColumn}`); } - assert.ok(!message.suggestions || hasOwnProperty(error, "suggestions"), `Error should have suggestions on error with message: "${message.message}`); + assert.ok(!message.suggestions || hasOwnProperty(error, "suggestions"), `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`); if (hasOwnProperty(error, "suggestions")) { // Support asserting there are no suggestions diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index b3327b83f42..8c68ac7054f 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -1975,7 +1975,7 @@ describe("RuleTester", () => { errors: [{ message: "Avoid using identifiers named 'foo'." }] }] }); - }, "Error should have suggestions on error with message: \"Avoid using identifiers named 'foo'."); + }, "Error at index 0 has suggestions. Please specify 'suggestions' property on the test error object."); }); it("should pass with valid suggestions (tested using desc)", () => { From e4592d479bb0699ec1dfac2c92df9c2298f01a21 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Sat, 20 Jan 2024 20:59:51 +0100 Subject: [PATCH 15/24] feat: support specifying an suggestion amount --- lib/rule-tester/rule-tester.js | 155 ++++++++++++++------------- tests/lib/rule-tester/rule-tester.js | 32 +++++- 2 files changed, 110 insertions(+), 77 deletions(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 7be6d42e359..544011a29c7 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -996,94 +996,99 @@ class RuleTester { if (hasOwnProperty(error, "suggestions")) { // Support asserting there are no suggestions - if (!error.suggestions || (Array.isArray(error.suggestions) && error.suggestions.length === 0)) { - if (Array.isArray(message.suggestions) && message.suggestions.length > 0) { - assert.fail(`Error should have no suggestions on error with message: "${message.message}"`); - } + if (!message.suggestions) { + assert.ok(!error.suggestions || (Array.isArray(error.suggestions) && error.suggestions.length === 0), `Error should have no suggestions on error with message: "${message.message}"`); + } else if (!error.suggestions || Array.isArray(error.suggestions) && error.suggestions.length === 0) { + assert.ok(message.suggestions.length === 0, `Error should have suggestions on error with message: "${message.message}"`); } else { - assert.strictEqual(Array.isArray(message.suggestions), true, `Error should have an array of suggestions. Instead received "${message.suggestions}" on error with message: "${message.message}"`); - assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`); - - error.suggestions.forEach((expectedSuggestion, index) => { - assert.ok( - typeof expectedSuggestion === "object" && expectedSuggestion !== null, - "Test suggestion in 'suggestions' array must be an object." - ); - Object.keys(expectedSuggestion).forEach(propertyName => { - assert.ok( - suggestionObjectParameters.has(propertyName), - `Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.` - ); - }); + if (typeof error.suggestions === "number") { + assert.strictEqual(message.suggestions.length, error.suggestions, `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions.length} suggestions`); + } else if (Array.isArray(error.suggestions)) { + assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`); - const actualSuggestion = message.suggestions[index]; - const suggestionPrefix = `Error Suggestion at index ${index}:`; - - if (hasOwnProperty(expectedSuggestion, "desc")) { - assert.ok( - !hasOwnProperty(expectedSuggestion, "data"), - `${suggestionPrefix} Test should not specify both 'desc' and 'data'.` - ); + error.suggestions.forEach((expectedSuggestion, index) => { assert.ok( - !hasOwnProperty(expectedSuggestion, "messageId"), - `${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.` + typeof expectedSuggestion === "object" && expectedSuggestion !== null, + "Test suggestion in 'suggestions' array must be an object." ); - assert.strictEqual( - actualSuggestion.desc, - expectedSuggestion.desc, - `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.` - ); - } else if (hasOwnProperty(expectedSuggestion, "messageId")) { - assert.ok( - ruleHasMetaMessages, - `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.` - ); - assert.ok( - hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId), - `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.` - ); - assert.strictEqual( - actualSuggestion.messageId, - expectedSuggestion.messageId, - `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.` - ); - if (hasOwnProperty(expectedSuggestion, "data")) { - const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId]; - const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data); + Object.keys(expectedSuggestion).forEach(propertyName => { + assert.ok( + suggestionObjectParameters.has(propertyName), + `Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.` + ); + }); + const actualSuggestion = message.suggestions[index]; + const suggestionPrefix = `Error Suggestion at index ${index}:`; + + if (hasOwnProperty(expectedSuggestion, "desc")) { + assert.ok( + !hasOwnProperty(expectedSuggestion, "data"), + `${suggestionPrefix} Test should not specify both 'desc' and 'data'.` + ); + assert.ok( + !hasOwnProperty(expectedSuggestion, "messageId"), + `${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.` + ); assert.strictEqual( actualSuggestion.desc, - rehydratedDesc, - `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".` + expectedSuggestion.desc, + `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.` + ); + } else if (hasOwnProperty(expectedSuggestion, "messageId")) { + assert.ok( + ruleHasMetaMessages, + `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.` + ); + assert.ok( + hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId), + `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.` + ); + assert.strictEqual( + actualSuggestion.messageId, + expectedSuggestion.messageId, + `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.` + ); + if (hasOwnProperty(expectedSuggestion, "data")) { + const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId]; + const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data); + + assert.strictEqual( + actualSuggestion.desc, + rehydratedDesc, + `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".` + ); + } + } else if (hasOwnProperty(expectedSuggestion, "data")) { + assert.fail( + `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.` + ); + } else { + assert.fail( + `${suggestionPrefix} Test must specify either 'messageId' or 'desc'.` ); } - } else if (hasOwnProperty(expectedSuggestion, "data")) { - assert.fail( - `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.` - ); - } else { - assert.fail( - `${suggestionPrefix} Test must specify either 'messageId' or 'desc'.` - ); - } - assert.ok(hasOwnProperty(expectedSuggestion, "output"), `${suggestionPrefix} The "output" property is required.`); - const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output; + assert.ok(hasOwnProperty(expectedSuggestion, "output"), `${suggestionPrefix} The "output" property is required.`); + const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output; - // Verify if suggestion fix makes a syntax error or not. - const errorMessageInSuggestion = - linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal); + // Verify if suggestion fix makes a syntax error or not. + const errorMessageInSuggestion = + linter.verify(codeWithAppliedSuggestion, result.configs, 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(!errorMessageInSuggestion, [ + "A fatal parsing error occurred in suggestion fix.", + `Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`, + "Suggestion output:", + codeWithAppliedSuggestion + ].join("\n")); - assert.strictEqual(codeWithAppliedSuggestion, expectedSuggestion.output, `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`); - assert.notStrictEqual(expectedSuggestion.output, item.code, `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`); - }); + assert.strictEqual(codeWithAppliedSuggestion, expectedSuggestion.output, `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`); + assert.notStrictEqual(expectedSuggestion.output, item.code, `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`); + }); + } else { + assert.fail(`Error should have an array of suggestions or a number for the amount of suggestions. Instead received "${util.inspect(message.suggestions)}" on error with message: "${message.message}"`); + } } } } else { diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 8c68ac7054f..401b98a5d5f 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2180,7 +2180,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error should have no suggestions on error with message: \"Avoid using identifiers named 'foo'.\""); + }, "Error should have suggestions on error with message: \"Avoid using identifiers named 'foo'.\""); }); }); @@ -2198,10 +2198,38 @@ describe("RuleTester", () => { }] }] }); - }, "Error should have an array of suggestions. Instead received \"undefined\" on error with message: \"Bad var.\""); + }, 'Error should have no suggestions on error with message: "Bad var."'); + }); + + it("should support specifying only the amount of suggestions", () => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", + suggestions: 1 + }] + }] + }); }); it("should fail when there are a different number of suggestions", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", + suggestions: 2 + }] + }] + }); + }, "Error should have 2 suggestions. Instead found 1 suggestions"); + }); + + it("should fail when there are a different number of suggestions for arrays", () => { assert.throws(() => { ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { valid: [], From 1da9f3c8dd9089f32cf1d2ea594809f51d4e1261 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Sat, 20 Jan 2024 21:04:43 +0100 Subject: [PATCH 16/24] fix: ecmaVersion should be a languageOptions property (not parserOptions) --- tests/lib/rules/array-callback-return.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/lib/rules/array-callback-return.js b/tests/lib/rules/array-callback-return.js index 390acca025e..938f73f7a2d 100644 --- a/tests/lib/rules/array-callback-return.js +++ b/tests/lib/rules/array-callback-return.js @@ -204,7 +204,7 @@ ruleTester.run("array-callback-return", rule, { { code: "foo.every(cb || function() {})", options: allowImplicitOptions, errors: ["Array.prototype.every() expects a return value from function."] }, { code: "[\"foo\",\"bar\"].sort(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.sort" } }] }, { code: "[\"foo\",\"bar\"].toSorted(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.toSorted" } }] }, - { code: "foo.forEach(x => x)", options: allowImplicitCheckForEach, languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach(x => {x})" }] }] }, + { code: "foo.forEach(x => x)", options: allowImplicitCheckForEach, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach(x => {x})" }] }] }, { code: "foo.forEach(function(x) { if (a == b) {return x;}})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, { code: "foo.forEach(function bar(x) { return x;})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function 'bar'", arrayMethodName: "Array.prototype.forEach" } }] }, @@ -282,10 +282,10 @@ ruleTester.run("array-callback-return", rule, { { code: "foo.filter(function foo() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, { code: "foo.filter(function foo() { return; })", options: checkForEachOptions, errors: [{ messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, { code: "foo.every(cb || function() {})", options: checkForEachOptions, errors: ["Array.prototype.every() expects a return value from function."] }, - { code: "foo.forEach((x) => void x)", options: checkForEachOptions, languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((x) => {void x})" }] }] }, - { code: "foo.forEach((x) => void bar(x))", options: checkForEachOptions, languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((x) => {void bar(x)})" }] }] }, - { code: "foo.forEach((x) => { return void bar(x); })", options: checkForEachOptions, languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, - { code: "foo.forEach((x) => { if (a === b) { return void a; } bar(x) })", options: checkForEachOptions, languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach((x) => void x)", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((x) => {void x})" }] }] }, + { code: "foo.forEach((x) => void bar(x))", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((x) => {void bar(x)})" }] }] }, + { code: "foo.forEach((x) => { return void bar(x); })", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach((x) => { if (a === b) { return void a; } bar(x) })", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, // options: { checkForEach: true, allowVoid: true } From afb1a9ead8b866d983805922b3c6308c156aaec4 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Sat, 20 Jan 2024 21:07:27 +0100 Subject: [PATCH 17/24] chore: tweak message for omitting output if there is no autofix --- lib/rule-tester/rule-tester.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 544011a29c7..f5bff0f32b4 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -1108,7 +1108,7 @@ class RuleTester { ); } else { assert.strictEqual(result.output, item.output, "Output is incorrect."); - assert.notStrictEqual(item.code, item.output, "Use 'output: null' if the rule does not fix this case instead of copying the code."); + assert.notStrictEqual(item.code, item.output, "Either omit the 'output' property or set it to null if there is no autofix."); } } else { assert.strictEqual( From 820405ea98cabdc103375c5575759299f2f4b3ad Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Sat, 20 Jan 2024 23:15:59 +0100 Subject: [PATCH 18/24] chore: fixup using old error message for the output property and update its docs --- docs/src/integrate/nodejs-api.md | 2 +- tests/lib/rule-tester/rule-tester.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/integrate/nodejs-api.md b/docs/src/integrate/nodejs-api.md index 181d93f3d98..2bcd0d5a205 100644 --- a/docs/src/integrate/nodejs-api.md +++ b/docs/src/integrate/nodejs-api.md @@ -788,7 +788,7 @@ Suggestions can be tested by defining a `suggestions` key on an errors object. T * `desc` (string): The suggestion `desc` value. Must provide this or `messageId` * `messageId` (string): The suggestion `messageId` value for suggestions that use `messageId`s. Must provide this or `desc` -* `data` (object): Placeholder data which can be used in combination with `messageId`. Required if `messageId` is specified and its message uses placeholders +* `data` (object): Placeholder data which can be used in combination with `messageId`. * `output` (string, required): A code string representing the result of applying the suggestion fix to the input code Example: diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 401b98a5d5f..0d4cd56ca51 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -793,7 +793,7 @@ describe("RuleTester", () => { { code: "eval('')", output: "eval('')", errors: 1 } ] }); - }, /Use 'output: null' if the rule does not fix this case instead of copying the code\./u); + }, "Either omit the 'output' property or set it to null if there is no autofix."); }); it("should throw an error when the expected output isn't specified and problems produce output", () => { From 3fb10c5d8cd1162079fe8bc5b683f42d986c990d Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Fri, 26 Jan 2024 17:42:21 +0100 Subject: [PATCH 19/24] fix: fixup reported error messages --- lib/rule-tester/rule-tester.js | 8 ++++---- tests/lib/rule-tester/rule-tester.js | 21 ++++++++++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index f5bff0f32b4..eac99e2ef6f 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -997,9 +997,9 @@ class RuleTester { // Support asserting there are no suggestions if (!message.suggestions) { - assert.ok(!error.suggestions || (Array.isArray(error.suggestions) && error.suggestions.length === 0), `Error should have no suggestions on error with message: "${message.message}"`); + assert.ok(!error.suggestions || (Array.isArray(error.suggestions) && error.suggestions.length === 0), `Error should have suggestions on error with message: "${message.message}"`); } else if (!error.suggestions || Array.isArray(error.suggestions) && error.suggestions.length === 0) { - assert.ok(message.suggestions.length === 0, `Error should have suggestions on error with message: "${message.message}"`); + assert.ok(message.suggestions.length === 0, `Error should have no suggestions on error with message: "${message.message}"`); } else { if (typeof error.suggestions === "number") { assert.strictEqual(message.suggestions.length, error.suggestions, `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions.length} suggestions`); @@ -1087,7 +1087,7 @@ class RuleTester { assert.notStrictEqual(expectedSuggestion.output, item.code, `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`); }); } else { - assert.fail(`Error should have an array of suggestions or a number for the amount of suggestions. Instead received "${util.inspect(message.suggestions)}" on error with message: "${message.message}"`); + assert.fail("Test error object property 'suggestions' should be an array or a number"); } } } @@ -1108,7 +1108,7 @@ class RuleTester { ); } else { assert.strictEqual(result.output, item.output, "Output is incorrect."); - assert.notStrictEqual(item.code, item.output, "Either omit the 'output' property or set it to null if there is no autofix."); + assert.notStrictEqual(item.code, item.output, "Test error object 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null."); } } else { assert.strictEqual( diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 0d4cd56ca51..983b929fb62 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -793,7 +793,7 @@ describe("RuleTester", () => { { code: "eval('')", output: "eval('')", errors: 1 } ] }); - }, "Either omit the 'output' property or set it to null if there is no autofix."); + }, "Test error object 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null."); }); it("should throw an error when the expected output isn't specified and problems produce output", () => { @@ -2180,7 +2180,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error should have suggestions on error with message: \"Avoid using identifiers named 'foo'.\""); + }, "Error should have no suggestions on error with message: \"Avoid using identifiers named 'foo'.\""); }); }); @@ -2198,7 +2198,7 @@ describe("RuleTester", () => { }] }] }); - }, 'Error should have no suggestions on error with message: "Bad var."'); + }, 'Error should have suggestions on error with message: "Bad var."'); }); it("should support specifying only the amount of suggestions", () => { @@ -2250,6 +2250,21 @@ describe("RuleTester", () => { }, "Error should have 2 suggestions. Instead found 1 suggestions"); }); + it("should fail when the suggestion property is neither a number nor an array", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", + suggestions: "1" + }] + }] + }); + }, "Test error object property 'suggestions' should be an array or a number"); + }); + it("should throw if suggestion fix made a syntax error.", () => { assert.throw(() => { ruleTester.run( From d57d3c175bc227c240b706da7bd3bd787ebce6df Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Tue, 30 Jan 2024 17:33:48 +0100 Subject: [PATCH 20/24] fix: refine more assertion messages --- lib/rule-tester/rule-tester.js | 4 ++-- tests/lib/rule-tester/rule-tester.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index eac99e2ef6f..f84b48a2528 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -969,7 +969,7 @@ class RuleTester { ); } } else { - assert.fail("Error must have either a 'messageId' or 'message'."); + assert.fail("Test error must specify either a 'messageId' or 'message'."); } if (error.type) { @@ -1108,7 +1108,7 @@ class RuleTester { ); } else { assert.strictEqual(result.output, item.output, "Output is incorrect."); - assert.notStrictEqual(item.code, item.output, "Test error object 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null."); + assert.notStrictEqual(item.code, item.output, "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null."); } } else { assert.strictEqual( diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 983b929fb62..6d74bd328e5 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -793,7 +793,7 @@ describe("RuleTester", () => { { code: "eval('')", output: "eval('')", errors: 1 } ] }); - }, "Test error object 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null."); + }, "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null."); }); it("should throw an error when the expected output isn't specified and problems produce output", () => { @@ -1040,7 +1040,7 @@ describe("RuleTester", () => { errors: [{}] }] }); - }, "Error must have either a 'messageId' or 'message'."); + }, "Test error must specify either a 'messageId' or 'message'."); }); it("should throw an error if an error has a property besides message or messageId", () => { @@ -1052,7 +1052,7 @@ describe("RuleTester", () => { errors: [{ line: 1 }] }] }); - }, "Error must have either a 'messageId' or 'message'."); + }, "Test error must specify either a 'messageId' or 'message'."); }); it("should pass-through the globals config of valid tests to the to rule", () => { @@ -1917,7 +1917,7 @@ describe("RuleTester", () => { valid: [], invalid: [{ code: "foo", errors: [{ data: "something" }] }] }); - }, "Error must have either a 'messageId' or 'message'."); + }, "Test error must specify either a 'messageId' or 'message'."); }); // fixable rules with or without `meta` property From 4453f419d0b9e9233b80c9176b9088600dbb4a3d Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Tue, 30 Jan 2024 20:36:30 +0100 Subject: [PATCH 21/24] docs: document suggestion can also be a number --- docs/src/integrate/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/integrate/nodejs-api.md b/docs/src/integrate/nodejs-api.md index 2bcd0d5a205..ce0890c79b6 100644 --- a/docs/src/integrate/nodejs-api.md +++ b/docs/src/integrate/nodejs-api.md @@ -784,7 +784,7 @@ Please note that `data` in a test case does not assert `data` passed to `context ### Testing Suggestions -Suggestions can be tested by defining a `suggestions` key on an errors object. The options to check for the suggestions are the following: +Suggestions can be tested by defining a `suggestions` key on an errors object. If this is a number, it asserts the number of suggestions provided for the error. Otherwise, this should be an array of objects, each containing information about a single provided suggestion. The following properties can be used: * `desc` (string): The suggestion `desc` value. Must provide this or `messageId` * `messageId` (string): The suggestion `messageId` value for suggestions that use `messageId`s. Must provide this or `desc` From cb2f783eea9adcdaa07726a3f11e4b90b64d4d5b Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Tue, 30 Jan 2024 20:36:39 +0100 Subject: [PATCH 22/24] chore: simplify suggestion existence checks --- lib/rule-tester/rule-tester.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index f84b48a2528..e98b9f98825 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -996,11 +996,13 @@ class RuleTester { if (hasOwnProperty(error, "suggestions")) { // Support asserting there are no suggestions - if (!message.suggestions) { - assert.ok(!error.suggestions || (Array.isArray(error.suggestions) && error.suggestions.length === 0), `Error should have suggestions on error with message: "${message.message}"`); - } else if (!error.suggestions || Array.isArray(error.suggestions) && error.suggestions.length === 0) { - assert.ok(message.suggestions.length === 0, `Error should have no suggestions on error with message: "${message.message}"`); - } else { + const expectsSuggestions = Array.isArray(error.suggestions) ? error.suggestions.length > 0 : Boolean(error.suggestions); + const hasSuggestions = message.suggestions !== void 0; + + if (!hasSuggestions && expectsSuggestions) { + assert.ok(!error.suggestions, `Error should have suggestions on error with message: "${message.message}"`); + } else if (hasSuggestions) { + assert.ok(expectsSuggestions, `Error should have no suggestions on error with message: "${message.message}"`); if (typeof error.suggestions === "number") { assert.strictEqual(message.suggestions.length, error.suggestions, `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions.length} suggestions`); } else if (Array.isArray(error.suggestions)) { From d4cca7f7482f823c73960d5dda39eb796c6c8e91 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Wed, 31 Jan 2024 14:32:35 +0100 Subject: [PATCH 23/24] feat: check string and regexp error matchers for missing suggestion matchers --- lib/rule-tester/rule-tester.js | 1 + .../testers/rule-tester/suggestions.js | 14 +++++++++++++ tests/lib/rule-tester/rule-tester.js | 20 +++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index e98b9f98825..4a0f8b37298 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -920,6 +920,7 @@ class RuleTester { // Just an error message. assertMessageMatches(message.message, error); + assert.ok(message.suggestions === void 0 || message.suggestions.length === 0, `The message has untested. suggestions. Please convert the error at index ${i} into an object by assigning this message to a 'message' property.`); } else if (typeof error === "object" && error !== null) { /* diff --git a/tests/fixtures/testers/rule-tester/suggestions.js b/tests/fixtures/testers/rule-tester/suggestions.js index b46258fa04e..34f404d26d8 100644 --- a/tests/fixtures/testers/rule-tester/suggestions.js +++ b/tests/fixtures/testers/rule-tester/suggestions.js @@ -184,3 +184,17 @@ module.exports.withFixerWithoutChanges = { }; } }; + +module.exports.withFailingFixer = { + create(context) { + return { + Identifier(node) { + context.report({ + node, + message: "some message", + suggest: [{ desc: "some suggestion", fix: fixer => null }] + }); + } + }; + } +}; diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 6d74bd328e5..0108cba4b42 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -636,6 +636,26 @@ describe("RuleTester", () => { }); }); + it("should not throw an error when the error is a string and the suggestion fixer is failing", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/suggestions").withFailingFixer, { + valid: [], + invalid: [ + { code: "foo", errors: ["some message"] } + ] + }); + }); + + it("throws an error when the error is a string and the suggestion fixer provides a fix", () => { + assert.throws(() => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [ + { code: "foo", errors: ["Avoid using identifiers named 'foo'."] } + ] + }); + }, "The message has untested. suggestions. Please convert the error at index 0 into an object by assigning this message to a 'message' property."); + }); + it("should throw an error when the error is an object with an unknown property name", () => { assert.throws(() => { ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { From 80d190f1f77ef0ec9c97ad023026d5e8b565db21 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Thu, 1 Feb 2024 13:49:27 +0100 Subject: [PATCH 24/24] chore: cleanup message for missing suggestions when testing only the message --- lib/rule-tester/rule-tester.js | 2 +- tests/lib/rule-tester/rule-tester.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 4a0f8b37298..25957eefe5a 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -920,7 +920,7 @@ class RuleTester { // Just an error message. assertMessageMatches(message.message, error); - assert.ok(message.suggestions === void 0 || message.suggestions.length === 0, `The message has untested. suggestions. Please convert the error at index ${i} into an object by assigning this message to a 'message' property.`); + assert.ok(message.suggestions === void 0, `Error at index ${i} has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions.`); } else if (typeof error === "object" && error !== null) { /* diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 0108cba4b42..47c19bd3432 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -653,7 +653,7 @@ describe("RuleTester", () => { { code: "foo", errors: ["Avoid using identifiers named 'foo'."] } ] }); - }, "The message has untested. suggestions. Please convert the error at index 0 into an object by assigning this message to a 'message' property."); + }, "Error at index 0 has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions."); }); it("should throw an error when the error is an object with an unknown property name", () => {