From f68e1800ba250344a423e9c1839a01458e64392e Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Fri, 10 May 2024 17:16:59 +0000 Subject: [PATCH 01/17] feat(rule-tester): Stricter rule test validations --- packages/rule-tester/src/RuleTester.ts | 312 +++++++++-------- .../tests/eslint-base/eslint-base.test.js | 331 +++++++++++++----- .../tests/eslint-base/fixtures/suggestions.js | 34 ++ 3 files changed, 458 insertions(+), 219 deletions(-) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 059702547dc1..076d2b05b436 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -29,6 +29,7 @@ import type { NormalizedRunTests, RuleTesterConfig, RunTests, + SuggestionOutput, TesterConfigWithDefaults, ValidTestCase, } from './types'; @@ -528,7 +529,17 @@ export class RuleTester extends TestFramework { config = merge(config, 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; } @@ -825,6 +836,10 @@ export class RuleTester extends TestFramework { if (typeof error === 'string' || error instanceof RegExp) { // Just an error message. assertMessageMatches(message.message, error); + 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) { /* * Error object. @@ -902,15 +917,12 @@ export class RuleTester extends TestFramework { `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`, ); } + } else { + assert.fail( + "Test error must specify 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, @@ -951,149 +963,162 @@ export class RuleTester extends TestFramework { ); } + 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 - if ( - !error.suggestions || - (isReadonlyArray(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}"`, - ); - } - } else { - assert( - Array.isArray(message.suggestions), - `Error should have an array of suggestions. Instead received "${String( - message.suggestions, - )}" on error with message: "${message.message}"`, + 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}"`, ); - const messageSuggestions = message.suggestions; - assert.strictEqual( - messageSuggestions.length, - error.suggestions.length, - `Error should have ${error.suggestions.length} suggestions. Instead found ${messageSuggestions.length} suggestions`, + } else if (hasSuggestions) { + assert.ok( + expectsSuggestions, + `Error should have no suggestions on error with message: "${message.message}"`, ); - - error.suggestions.forEach((expectedSuggestion, index) => { - assert.ok( - typeof expectedSuggestion === 'object' && - expectedSuggestion != null, - "Test suggestion in 'suggestions' array must be an object.", + 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`, ); - Object.keys(expectedSuggestion).forEach(propertyName => { - assert.ok( - SUGGESTION_OBJECT_PARAMETERS.has(propertyName), - `Invalid suggestion property name '${propertyName}'. Expected one of ${FRIENDLY_SUGGESTION_OBJECT_PARAMETER_LIST}.`, - ); - }); - - const actualSuggestion = messageSuggestions[index]; - const suggestionPrefix = `Error Suggestion at index ${index} :`; - - // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` - if (hasOwnProperty(expectedSuggestion, 'desc')) { - assert.ok( - !hasOwnProperty(expectedSuggestion, 'data'), - `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`, - ); - // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` - const expectedDesc = expectedSuggestion.desc as string; - assert.strictEqual( - actualSuggestion.desc, - expectedDesc, - `${suggestionPrefix} desc should be "${expectedDesc}" but got "${actualSuggestion.desc}" instead.`, - ); - } - - 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.`, - ); - - const unsubstitutedPlaceholders = - getUnsubstitutedMessagePlaceholders( - actualSuggestion.desc, - rule.meta.messages[expectedSuggestion.messageId], - expectedSuggestion.data, - ); - assert.ok( - unsubstitutedPlaceholders.length === 0, - `The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(', ')}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? 'values' : 'value'} via the 'data' property for the suggestion in the context.report() call.`, - ); - - if (hasOwnProperty(expectedSuggestion, 'data')) { - const unformattedMetaMessage = - rule.meta.messages[expectedSuggestion.messageId]; - const rehydratedDesc = interpolate( - unformattedMetaMessage, - expectedSuggestion.data, + error.suggestions!.forEach( + (expectedSuggestion: SuggestionOutput, index) => { + assert.ok( + typeof expectedSuggestion === 'object' && + expectedSuggestion !== null, + "Test suggestion in 'suggestions' array must be an object.", + ); + // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` + const expectedDesc = expectedSuggestion.desc as string; + Object.keys(expectedSuggestion).forEach(propertyName => { + assert.ok( + SUGGESTION_OBJECT_PARAMETERS.has(propertyName), + `Invalid suggestion property name '${propertyName}'. Expected one of ${FRIENDLY_SUGGESTION_OBJECT_PARAMETER_LIST}.`, + ); + }); + + const actualSuggestion = message.suggestions![index]; + const suggestionPrefix = `Error Suggestion at index ${index}:`; + + // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` + 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, + expectedDesc, + `${suggestionPrefix} desc should be "${expectedDesc}" 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'.`, + ); + } + + 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 = this.#linter + .verify( + codeWithAppliedSuggestion, + result.config, + result.filename, + ) + .find(m => m.fatal); + + assert( + !errorMessageInSuggestion, + [ + 'A fatal parsing error occurred in suggestion fix.', + `Error: ${errorMessageInSuggestion?.message}`, + 'Suggestion output:', + codeWithAppliedSuggestion, + ].join('\n'), ); assert.strictEqual( - actualSuggestion.desc, - rehydratedDesc, - `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`, - ); - } - } else { - assert.ok( - !hasOwnProperty(expectedSuggestion, 'data'), - `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`, - ); - } - - if (hasOwnProperty(expectedSuggestion, 'output')) { - const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes( - item.code, - [actualSuggestion], - ).output; - - // Verify if suggestion fix makes a syntax error or not. - const errorMessageInSuggestion = this.#linter - .verify( - codeWithAppliedSuggestion, - result.config, - result.filename, - ) - .find(m => m.fatal); - - assert( - !errorMessageInSuggestion, - [ - 'A fatal parsing error occurred in suggestion fix.', - `Error: ${errorMessageInSuggestion?.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}"`, - ); - } - }); + 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( + "Test error object property 'suggestions' should be an array or a number", + ); + } } } } else { @@ -1123,6 +1148,11 @@ export class RuleTester extends TestFramework { item.code, "The rule fixed the code. Please add 'output' property.", ); + 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.", + ); } assertASTDidntChange(result.beforeAST, result.afterAST); diff --git a/packages/rule-tester/tests/eslint-base/eslint-base.test.js b/packages/rule-tester/tests/eslint-base/eslint-base.test.js index a50654f922aa..d860caad8d70 100644 --- a/packages/rule-tester/tests/eslint-base/eslint-base.test.js +++ b/packages/rule-tester/tests/eslint-base/eslint-base.test.js @@ -473,7 +473,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); @@ -529,6 +529,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/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/suggestions").basic, { + valid: [], + invalid: [ + { code: "foo", errors: ["Avoid using identifiers named 'foo'."] } + ] + }); + }, "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", () => { assert.throws(() => { ruleTester.run("no-var", require("./fixtures/no-var"), { @@ -678,6 +698,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/no-eval"), { + valid: [], + invalid: [ + { code: "eval('')", output: "eval('')", errors: 1 } + ] + }); + }, "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", () => { assert.throws(() => { ruleTester.run("no-var", require("./fixtures/no-var"), { @@ -913,14 +944,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/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/no-eval"), { + valid: ["Eval(foo)"], + invalid: [{ + code: "eval(foo)", + errors: [{}] + }] + }); + }, "Test error must specify 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/no-eval"), { + valid: ["Eval(foo)"], + invalid: [{ + code: "eval(foo)", + errors: [{ line: 1 }] + }] + }); + }, "Test error must specify either a 'messageId' or 'message'."); }); it("should pass-through the globals config of valid tests to the to rule", () => { @@ -1048,7 +1093,7 @@ describe("RuleTester", () => { { code: "eval(foo)", parser: require.resolve("esprima"), - errors: [{ line: 1 }] + errors: [{ message: "eval sucks.", line: 1 }] } ] }); @@ -1774,10 +1819,24 @@ describe("RuleTester", () => { valid: [], invalid: [{ code: "foo", errors: [{ data: "something" }] }] }); - }, "Error must specify 'messageId' if 'data' is used."); + }, "Test error must specify either a 'messageId' or 'message'."); }); describe("suggestions", () => { + it("should throw if suggestions are available but not specified", () => { + assert.throw(() => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [ + "var boo;" + ], + invalid: [{ + code: "var foo;", + errors: [{ 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)", () => { ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { valid: [ @@ -1786,6 +1845,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var bar;" @@ -1802,11 +1862,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}" @@ -1823,6 +1885,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -1841,6 +1904,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -1853,24 +1917,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/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename identifier 'foo' to 'baz'", - messageId: "renameFoo", - output: "var baz;" + it("should fail with valid suggestions when testing using both desc and messageIds for the same suggestion", () => { + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/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)", () => { @@ -1879,6 +1946,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var bar;" @@ -1897,6 +1965,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "bar" }, @@ -1967,16 +2036,34 @@ describe("RuleTester", () => { }); - it("should pass when tested using empty suggestion test objects if the array length is correct", () => { + 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/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{}, {}] - }] + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{}, {}] }] + }] + }); + }, "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", () => { + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/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", () => { @@ -1986,6 +2073,7 @@ describe("RuleTester", () => { invalid: [{ code: "eval('var foo');", errors: [{ + message: "eval sucks.", suggestions }] }] @@ -2001,6 +2089,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions }] }] @@ -2017,12 +2106,26 @@ describe("RuleTester", () => { code: "var foo;", errors: [{ suggestions: [{ + message: "Bad var.", messageId: "this-does-not-exist" }] }] }] }); - }, "Error should have an array of suggestions. Instead received \"undefined\" 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", () => { + ruleTester.run("suggestions-basic", require("./fixtures/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", () => { @@ -2032,6 +2135,22 @@ describe("RuleTester", () => { 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/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var bar;" @@ -2087,85 +2206,76 @@ describe("RuleTester", () => { }, /A fatal parsing error occurred in suggestion fix\.\nError: .+\nSuggestion output:\n.+/u); }); - it("should throw if the suggestion description doesn't match", () => { + it("should fail when the suggestion property is neither a number nor an array", () => { assert.throws(() => { ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { valid: [], invalid: [{ code: "var foo;", errors: [{ - suggestions: [{ - desc: "not right", - output: "var baz;" - }] + message: "Avoid using identifiers named 'foo'.", + suggestions: "1" }] }] }); - }, "Error Suggestion at index 0 : desc should be \"not right\" but got \"Rename identifier 'foo' to 'bar'\" instead."); + }, "Test error object property 'suggestions' should be an array or a number"); }); - it("should throw if the suggestion description doesn't match (although messageIds match)", () => { + it("should throw if the suggestion description doesn't match", () => { assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { valid: [], invalid: [{ code: "var foo;", errors: [{ suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename id 'foo' to 'baz'", - messageId: "renameFoo", + desc: "not right", output: "var baz;" }] }] }] }); - }, "Error Suggestion at index 1 : desc should be \"Rename id 'foo' to 'baz'\" but got \"Rename identifier 'foo' to 'baz'\" instead."); + }, "Error Suggestion at index 0 : desc should be \"not right\" but got \"Rename identifier 'foo' to 'bar'\" instead."); }); - it("should throw if the suggestion messageId doesn't match", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - messageId: "unused", - output: "var bar;" - }, { - messageId: "renameFoo", - output: "var baz;" - }] + it("should pass when different suggestion matchers use desc and messageId", () => { + ruleTester.run("suggestions-messageIds", require("./fixtures/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 0 : messageId should be 'unused' but got 'renameFoo' instead."); + }] + }); }); - it("should throw if the suggestion messageId doesn't match (although descriptions match)", () => { + it("should throw if the suggestion messageId doesn't match", () => { assert.throws(() => { ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { valid: [], invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", + messageId: "unused", output: "var bar;" }, { - desc: "Rename identifier 'foo' to 'baz'", - messageId: "avoidFoo", + messageId: "renameFoo", output: "var baz;" }] }] }] }); - }, "Error Suggestion at index 1 : messageId should be 'avoidFoo' 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", () => { @@ -2175,6 +2285,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2182,7 +2293,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", () => { @@ -2192,6 +2303,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2202,7 +2314,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)", () => { @@ -2212,6 +2324,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "car" }, @@ -2224,7 +2337,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)", () => { @@ -2234,6 +2347,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "bar" }, @@ -2246,7 +2360,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", () => { @@ -2256,6 +2370,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", messageId: "renameFoo", @@ -2269,7 +2384,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", () => { @@ -2279,6 +2394,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "bar" }, @@ -2290,7 +2406,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", () => { @@ -2300,6 +2416,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var baz;" @@ -2310,6 +2427,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/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/suggestions").basic, { @@ -2317,6 +2452,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [null] }] }] @@ -2329,6 +2465,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "avoidFoo", suggestions: [ { messageId: "renameFoo", @@ -2351,6 +2488,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "avoidFoo", suggestions: [{ message: "Rename identifier 'foo' to 'bar'" }] @@ -2922,6 +3060,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/no-var"), { + valid: [{ code: "foo", only: "" }], + invalid: [] + }); + }, /Optional test case property 'only' must be a boolean/u); + + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/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/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/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; diff --git a/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js b/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js index 71781086f4b8..6310d0a2104a 100644 --- a/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js +++ b/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js @@ -167,6 +167,40 @@ 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') + }] + }); + } + } + }; + } +}; + +module.exports.withFailingFixer = { + create(context) { + return { + Identifier(node) { + context.report({ + node, + message: "some message", + suggest: [{ desc: "some suggestion", fix: fixer => null }] + }); + } + }; + } +}; + module.exports.withMissingPlaceholderData = { meta: { messages: { From e5db11f94d34a0599b3d1119afd2d91f7f8ce200 Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Mon, 13 May 2024 15:12:10 +0000 Subject: [PATCH 02/17] Linted eslint-base.test.js --- .../tests/eslint-base/eslint-base.test.js | 426 +++++++++--------- 1 file changed, 213 insertions(+), 213 deletions(-) diff --git a/packages/rule-tester/tests/eslint-base/eslint-base.test.js b/packages/rule-tester/tests/eslint-base/eslint-base.test.js index d860caad8d70..324a0564c3f9 100644 --- a/packages/rule-tester/tests/eslint-base/eslint-base.test.js +++ b/packages/rule-tester/tests/eslint-base/eslint-base.test.js @@ -530,23 +530,23 @@ 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/suggestions").withFailingFixer, { - valid: [], - invalid: [ - { code: "foo", errors: ["some message"] } - ] - }); + ruleTester.run("no-var", require("./fixtures/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/suggestions").basic, { - valid: [], - invalid: [ - { code: "foo", errors: ["Avoid using identifiers named 'foo'."] } - ] - }); - }, "Error at index 0 has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions."); + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/suggestions").basic, { + valid: [], + invalid: [ + { code: "foo", errors: ["Avoid using identifiers named 'foo'."] } + ] + }); + }, "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", () => { @@ -699,14 +699,14 @@ describe("RuleTester", () => { }); 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/no-eval"), { - valid: [], - invalid: [ - { code: "eval('')", output: "eval('')", errors: 1 } - ] - }); - }, "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null."); + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-eval"), { + valid: [], + invalid: [ + { code: "eval('')", output: "eval('')", errors: 1 } + ] + }); + }, "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", () => { @@ -1732,60 +1732,60 @@ describe("RuleTester", () => { }); it("should throw if the message has a single unsubstituted placeholder when data is not specified", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMissingData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] - }); - }, "The reported message has an unsubstituted placeholder 'name'. Please provide the missing value via the 'data' property in the context.report() call."); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMissingData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); + }, "The reported message has an unsubstituted placeholder 'name'. Please provide the missing value via the 'data' property in the context.report() call."); }); it("should throw if the message has a single unsubstituted placeholders when data is specified", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMissingData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "name" } }] }] - }); - }, "Hydrated message \"Avoid using variables named 'name'.\" does not match \"Avoid using variables named '{{ name }}'."); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMissingData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "name" } }] }] + }); + }, "Hydrated message \"Avoid using variables named 'name'.\" does not match \"Avoid using variables named '{{ name }}'."); }); it("should throw if the message has multiple unsubstituted placeholders when data is not specified", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMultipleMissingDataProperties, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] - }); - }, "The reported message has unsubstituted placeholders: 'type', 'name'. Please provide the missing values via the 'data' property in the context.report() call."); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMultipleMissingDataProperties, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); + }, "The reported message has unsubstituted placeholders: 'type', 'name'. Please provide the missing values via the 'data' property in the context.report() call."); }); it("should not throw if the data in the message contains placeholders not present in the raw message", () => { - ruleTester.run("foo", require("./fixtures/messageId").withPlaceholdersInData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] - }); + ruleTester.run("foo", require("./fixtures/messageId").withPlaceholdersInData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); }); it("should throw if the data in the message contains the same placeholder and data is not specified", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withSamePlaceholdersInData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] - }); - }, "The reported message has an unsubstituted placeholder 'name'. Please provide the missing value via the 'data' property in the context.report() call."); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withSamePlaceholdersInData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); + }, "The reported message has an unsubstituted placeholder 'name'. Please provide the missing value via the 'data' property in the context.report() call."); }); it("should not throw if the data in the message contains the same placeholder and data is specified", () => { - ruleTester.run("foo", require("./fixtures/messageId").withSamePlaceholdersInData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "{{ name }}" } }] }] - }); + ruleTester.run("foo", require("./fixtures/messageId").withSamePlaceholdersInData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "{{ name }}" } }] }] + }); }); it("should not throw an error for specifying non-string data values", () => { - ruleTester.run("foo", require("./fixtures/messageId").withNonStringData, { - valid: [], - invalid: [{ code: "0", errors: [{ messageId: "avoid", data: { value: 0 } }] }] - }); + ruleTester.run("foo", require("./fixtures/messageId").withNonStringData, { + valid: [], + invalid: [{ code: "0", errors: [{ messageId: "avoid", data: { value: 0 } }] }] + }); }); // messageId/message misconfiguration cases @@ -1981,89 +1981,89 @@ describe("RuleTester", () => { }); it("should fail with a single missing data placeholder when data is not specified", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMissingPlaceholderData, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - messageId: "avoidFoo", - suggestions: [{ - messageId: "renameFoo", - output: "var bar;" - }] - }] - }] - }); - }, "The message of the suggestion has an unsubstituted placeholder 'newName'. Please provide the missing value via the 'data' property for the suggestion in the context.report() call."); + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMissingPlaceholderData, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + messageId: "renameFoo", + output: "var bar;" + }] + }] + }] + }); + }, "The message of the suggestion has an unsubstituted placeholder 'newName'. Please provide the missing value via the 'data' property for the suggestion in the context.report() call."); }); it("should fail with a single missing data placeholder when data is specified", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMissingPlaceholderData, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - messageId: "avoidFoo", - suggestions: [{ - messageId: "renameFoo", - data: { other: "name" }, - output: "var bar;" - }] - }] - }] - }); - }, "The message of the suggestion has an unsubstituted placeholder 'newName'. Please provide the missing value via the 'data' property for the suggestion in the context.report() call."); + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMissingPlaceholderData, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + messageId: "renameFoo", + data: { other: "name" }, + output: "var bar;" + }] + }] + }] + }); + }, "The message of the suggestion has an unsubstituted placeholder 'newName'. Please provide the missing value via the 'data' property for the suggestion in the context.report() call."); }); it("should fail with multiple missing data placeholders when data is not specified", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMultipleMissingPlaceholderDataProperties, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - messageId: "avoidFoo", - suggestions: [{ - messageId: "rename", - output: "var bar;" - }] - }] - }] - }); - }, "The message of the suggestion has unsubstituted placeholders: 'currentName', 'newName'. Please provide the missing values via the 'data' property for the suggestion in the context.report() call."); + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMultipleMissingPlaceholderDataProperties, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + messageId: "rename", + output: "var bar;" + }] + }] + }] + }); + }, "The message of the suggestion has unsubstituted placeholders: 'currentName', 'newName'. Please provide the missing values via the 'data' property for the suggestion in the context.report() call."); }); 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/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - messageId: "avoidFoo", - suggestions: [{}, {}] - }] - }] - }); - }, "Error Suggestion at index 0: Test must specify either 'messageId' or 'desc'"); + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{}, {}] + }] + }] + }); + }, "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", () => { - assert.throw(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - messageId: "avoidFoo", - suggestions: [{ messageId: "renameFoo" }, {}] - }] - }] - }); - }, 'Error Suggestion at index 0: The "output" property is required.'); + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/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", () => { @@ -2116,16 +2116,16 @@ describe("RuleTester", () => { }); it("should support specifying only the amount of suggestions", () => { - ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - message: "Avoid using identifiers named 'foo'.", - suggestions: 1 - }] - }] - }); + ruleTester.run("suggestions-basic", require("./fixtures/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", () => { @@ -2428,21 +2428,21 @@ describe("RuleTester", () => { }); it("should throw if the resulting suggestion output is the same as the original source code", () => { - assert.throws(() => { - ruleTester.run("suggestions-basic", require("./fixtures/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'.\""); + assert.throws(() => { + ruleTester.run("suggestions-basic", require("./fixtures/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", () => { @@ -2519,37 +2519,37 @@ describe("RuleTester", () => { }); it("should fail if a rule produces two suggestions with the same description", () => { - assert.throws(() => { - ruleTester.run("suggestions-with-duplicate-descriptions", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateDescriptions, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - }, "Suggestion message 'Rename 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); - }); - - it("should fail if a rule produces two suggestions with the same messageId without data", () => { - assert.throws(() => { - ruleTester.run("suggestions-with-duplicate-messageids-no-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsNoData, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - }, "Suggestion message 'Rename identifier' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); - }); - - it("should fail if a rule produces two suggestions with the same messageId with data", () => { - assert.throws(() => { - ruleTester.run("suggestions-with-duplicate-messageids-with-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsWithData, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - }, "Suggestion message 'Rename identifier 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); - }); + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-descriptions", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateDescriptions, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + + it("should fail if a rule produces two suggestions with the same messageId without data", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-messageids-no-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsNoData, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename identifier' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + + it("should fail if a rule produces two suggestions with the same messageId with data", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-messageids-with-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsWithData, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename identifier 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); it("should throw an error if a rule that doesn't have `meta.hasSuggestions` enabled produces suggestions", () => { assert.throws(() => { @@ -3061,40 +3061,40 @@ describe("RuleTester", () => { }); describe("type checking", () => { - it('should throw if "only" property is not a boolean', () => { + 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/no-var"), { - valid: [{ code: "foo", only: "" }], - invalid: [] - }); - }, /Optional test case property 'only' must be a boolean/u); + // "only" has to be falsy as itOnly is not mocked for all test cases + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [{ code: "foo", only: "" }], + invalid: [] + }); + }, /Optional test case property 'only' must be a boolean/u); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: [], - invalid: [{ code: "foo", only: 0, errors: 1 }] - }); - }, /Optional test case property 'only' must be a boolean/u); - }); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/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/no-var"), { - valid: [{ code: "foo", filename: false }], - invalid: [] + it('should throw if "filename" property is not a string', () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [{ code: "foo", filename: false }], + invalid: [] - }); - }, /Optional test case property 'filename' must be a string/u); + }); + }, /Optional test case property 'filename' must be a string/u); - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/no-var"), { - valid: ["foo"], - invalid: [{ code: "foo", errors: 1, filename: 0 }] - }); - }, /Optional test case property 'filename' must be a string/u); - }); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/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", () => { From 184814f8571e174abf78d44836007abeaf8e9fd9 Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Mon, 13 May 2024 15:12:48 +0000 Subject: [PATCH 03/17] Fixed typing problems --- packages/rule-tester/src/RuleTester.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 076d2b05b436..14e53a6d8d32 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -973,6 +973,9 @@ export class RuleTester extends TestFramework { ? error.suggestions.length > 0 : Boolean(error.suggestions); const hasSuggestions = message.suggestions !== void 0; + // @ts-expect-error -- we purposely don't verify for undefined + const messageSuggestions: Linter.LintSuggestion[] = + message.suggestions; if (!hasSuggestions && expectsSuggestions) { assert.ok( @@ -986,22 +989,24 @@ export class RuleTester extends TestFramework { ); if (typeof error.suggestions === 'number') { assert.strictEqual( - message.suggestions!.length, + messageSuggestions.length, error.suggestions, - `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions!.length} suggestions`, + // It is possible that error.suggestions is a number + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Error should have ${error.suggestions} suggestions. Instead found ${messageSuggestions.length} suggestions`, ); } else if (Array.isArray(error.suggestions)) { assert.strictEqual( - message.suggestions!.length, + messageSuggestions.length, error.suggestions.length, - `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions!.length} suggestions`, + `Error should have ${error.suggestions.length} suggestions. Instead found ${messageSuggestions.length} suggestions`, ); - error.suggestions!.forEach( + error.suggestions.forEach( (expectedSuggestion: SuggestionOutput, index) => { assert.ok( typeof expectedSuggestion === 'object' && - expectedSuggestion !== null, + expectedSuggestion != null, "Test suggestion in 'suggestions' array must be an object.", ); // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` @@ -1013,7 +1018,7 @@ export class RuleTester extends TestFramework { ); }); - const actualSuggestion = message.suggestions![index]; + const actualSuggestion = messageSuggestions[index]; const suggestionPrefix = `Error Suggestion at index ${index}:`; // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` From 8342516f12d22a648e68883d5afc47da82329999 Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Mon, 13 May 2024 15:15:55 +0000 Subject: [PATCH 04/17] Forgotten lint --- packages/rule-tester/tests/eslint-base/eslint-base.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rule-tester/tests/eslint-base/eslint-base.test.js b/packages/rule-tester/tests/eslint-base/eslint-base.test.js index 324a0564c3f9..20d95917fc89 100644 --- a/packages/rule-tester/tests/eslint-base/eslint-base.test.js +++ b/packages/rule-tester/tests/eslint-base/eslint-base.test.js @@ -473,7 +473,7 @@ describe("RuleTester", () => { "bar = baz;" ], invalid: [ - { code: "var foo = bar; var baz = quux", errors: [{ message: "Bad var.", 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); From 7d3f0e74a7a2e7f5bca2574ba912c0538a2bcf2e Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Mon, 13 May 2024 15:34:01 +0000 Subject: [PATCH 05/17] Put back wrongly removed lines --- packages/rule-tester/src/RuleTester.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 14e53a6d8d32..7f3c69a84af8 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -1021,6 +1021,18 @@ export class RuleTester extends TestFramework { const actualSuggestion = messageSuggestions[index]; const suggestionPrefix = `Error Suggestion at index ${index}:`; + const unsubstitutedPlaceholders = + getUnsubstitutedMessagePlaceholders( + actualSuggestion.desc, + rule.meta.messages[expectedSuggestion.messageId], + expectedSuggestion.data, + ); + + assert.ok( + unsubstitutedPlaceholders.length === 0, + `The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(', ')}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? 'values' : 'value'} via the 'data' property for the suggestion in the context.report() call.`, + ); + // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` if (hasOwnProperty(expectedSuggestion, 'desc')) { assert.ok( From 0e0a68bee80e0b03e90d72a3b9c89ba1271612b9 Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Mon, 13 May 2024 19:46:45 +0000 Subject: [PATCH 06/17] Implemented missing suggestions --- .../tests/rules/no-floating-promises.test.ts | 1936 ++++++++++++++--- .../rules/no-unsafe-enum-comparison.test.ts | 142 +- .../tests/rules/prefer-as-const.test.ts | 6 + .../rules/prefer-nullish-coalescing.test.ts | 397 +++- .../rules/strict-boolean-expressions.test.ts | 480 +++- 5 files changed, 2653 insertions(+), 308 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts index e6087e512265..308e1ebbe4a6 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -749,18 +749,70 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + void Promise.resolve('value'); + Promise.resolve('value').then(() => {}); + Promise.resolve('value').catch(); + Promise.resolve('value').finally(); +} + `, + }, + ], }, { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.resolve('value'); + void Promise.resolve('value').then(() => {}); + Promise.resolve('value').catch(); + Promise.resolve('value').finally(); +} + `, + }, + ], }, { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.resolve('value'); + Promise.resolve('value').then(() => {}); + void Promise.resolve('value').catch(); + Promise.resolve('value').finally(); +} + `, + }, + ], }, { line: 6, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.resolve('value'); + Promise.resolve('value').then(() => {}); + Promise.resolve('value').catch(); + void Promise.resolve('value').finally(); +} + `, + }, + ], }, ], }, @@ -791,59 +843,293 @@ doSomething(); { line: 11, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + void obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 12, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + void obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 13, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + void obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 14, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + void obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 15, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + void obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 16, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + void obj6?.a.b.c?.(); + + callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 18, messageId: 'floatingVoid', - }, - { - line: 21, - messageId: 'floatingVoid', - }, - ], - }, - { - code: ` -declare const myTag: (strings: TemplateStringsArray) => Promise; -myTag\`abc\`; + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + void callback?.(); +}; + +doSomething(); `, - errors: [ - { - line: 3, - messageId: 'floatingVoid', + }, + ], }, - ], - }, - { - code: ` -declare const myTag: (strings: TemplateStringsArray) => Promise; -myTag\`abc\`.then(() => {}); - `, - errors: [ { - line: 3, + line: 21, messageId: 'floatingVoid', - }, + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); +}; + +void doSomething(); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const myTag: (strings: TemplateStringsArray) => Promise; +myTag\`abc\`; + `, + errors: [ + { + line: 3, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const myTag: (strings: TemplateStringsArray) => Promise; +void myTag\`abc\`; + `, + }, + ], + }, + ], + }, + { + code: ` +declare const myTag: (strings: TemplateStringsArray) => Promise; +myTag\`abc\`.then(() => {}); + `, + errors: [ + { + line: 3, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const myTag: (strings: TemplateStringsArray) => Promise; +void myTag\`abc\`.then(() => {}); + `, + }, + ], + }, ], }, { @@ -855,6 +1141,15 @@ myTag\`abc\`.finally(() => {}); { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const myTag: (strings: TemplateStringsArray) => Promise; +void myTag\`abc\`.finally(() => {}); + `, + }, + ], }, ], }, @@ -896,18 +1191,70 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + void Promise.reject(new Error('message')); + Promise.reject(new Error('message')).then(() => {}); + Promise.reject(new Error('message')).catch(); + Promise.reject(new Error('message')).finally(); +} + `, + }, + ], }, { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.reject(new Error('message')); + void Promise.reject(new Error('message')).then(() => {}); + Promise.reject(new Error('message')).catch(); + Promise.reject(new Error('message')).finally(); +} + `, + }, + ], }, { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.reject(new Error('message')); + Promise.reject(new Error('message')).then(() => {}); + void Promise.reject(new Error('message')).catch(); + Promise.reject(new Error('message')).finally(); +} + `, + }, + ], }, { line: 6, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.reject(new Error('message')); + Promise.reject(new Error('message')).then(() => {}); + Promise.reject(new Error('message')).catch(); + void Promise.reject(new Error('message')).finally(); +} + `, + }, + ], }, ], }, @@ -923,14 +1270,50 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + void (async () => true)(); + (async () => true)().then(() => {}); + (async () => true)().catch(); +} + `, + }, + ], }, { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + (async () => true)(); + void (async () => true)().then(() => {}); + (async () => true)().catch(); +} + `, + }, + ], }, { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + (async () => true)(); + (async () => true)().then(() => {}); + void (async () => true)().catch(); +} + `, + }, + ], }, ], }, @@ -949,18 +1332,78 @@ async function test() { { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + async function returnsPromise() {} + + void returnsPromise(); + returnsPromise().then(() => {}); + returnsPromise().catch(); + returnsPromise().finally(); +} + `, + }, + ], }, { line: 6, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + async function returnsPromise() {} + + returnsPromise(); + void returnsPromise().then(() => {}); + returnsPromise().catch(); + returnsPromise().finally(); +} + `, + }, + ], }, { line: 7, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + async function returnsPromise() {} + + returnsPromise(); + returnsPromise().then(() => {}); + void returnsPromise().catch(); + returnsPromise().finally(); +} + `, + }, + ], }, { line: 8, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + async function returnsPromise() {} + + returnsPromise(); + returnsPromise().then(() => {}); + returnsPromise().catch(); + void returnsPromise().finally(); +} + `, + }, + ], }, ], }, @@ -975,10 +1418,32 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + void (Math.random() > 0.5 ? Promise.resolve() : null); + Math.random() > 0.5 ? null : Promise.resolve(); +} + `, + }, + ], }, { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Math.random() > 0.5 ? Promise.resolve() : null; + void (Math.random() > 0.5 ? null : Promise.resolve()); +} + `, + }, + ], }, ], }, @@ -994,15 +1459,51 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + void (Promise.resolve(), 123); + 123, Promise.resolve(); + 123, Promise.resolve(), 123; +} + `, + }, + ], }, { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.resolve(), 123; + void (123, Promise.resolve()); + 123, Promise.resolve(), 123; +} + `, + }, + ], }, { line: 5, messageId: 'floatingVoid', - }, + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.resolve(), 123; + 123, Promise.resolve(); + void (123, Promise.resolve(), 123); +} + `, + }, + ], + }, ], }, { @@ -1171,6 +1672,17 @@ async function test() { { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + const obj = { foo: Promise.resolve() }; + void obj.foo; +} + `, + }, + ], }, ], }, @@ -1184,6 +1696,16 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + void new Promise(resolve => resolve()); +} + `, + }, + ], }, ], }, @@ -1202,18 +1724,78 @@ async function test() { { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseValue: Promise; + + void promiseValue; + promiseValue.then(() => {}); + promiseValue.catch(); + promiseValue.finally(); +} + `, + }, + ], }, { line: 6, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseValue: Promise; + + promiseValue; + void promiseValue.then(() => {}); + promiseValue.catch(); + promiseValue.finally(); +} + `, + }, + ], }, { line: 7, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseValue: Promise; + + promiseValue; + promiseValue.then(() => {}); + void promiseValue.catch(); + promiseValue.finally(); +} + `, + }, + ], }, { line: 8, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseValue: Promise; + + promiseValue; + promiseValue.then(() => {}); + promiseValue.catch(); + void promiseValue.finally(); +} + `, + }, + ], }, ], }, @@ -1229,6 +1811,18 @@ async function test() { { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseUnion: Promise | number; + + void promiseUnion; +} + `, + }, + ], }, ], }, @@ -1246,14 +1840,56 @@ async function test() { { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseIntersection: Promise & number; + + void promiseIntersection; + promiseIntersection.then(() => {}); + promiseIntersection.catch(); +} + `, + }, + ], }, { line: 6, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseIntersection: Promise & number; + + promiseIntersection; + void promiseIntersection.then(() => {}); + promiseIntersection.catch(); +} + `, + }, + ], }, { line: 7, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseIntersection: Promise & number; + + promiseIntersection; + promiseIntersection.then(() => {}); + void promiseIntersection.catch(); +} + `, + }, + ], }, ], }, @@ -1273,18 +1909,82 @@ async function test() { { line: 6, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + class CanThen extends Promise {} + const canThen: CanThen = Foo.resolve(2); + + void canThen; + canThen.then(() => {}); + canThen.catch(); + canThen.finally(); +} + `, + }, + ], }, { line: 7, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + class CanThen extends Promise {} + const canThen: CanThen = Foo.resolve(2); + + canThen; + void canThen.then(() => {}); + canThen.catch(); + canThen.finally(); +} + `, + }, + ], }, { line: 8, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + class CanThen extends Promise {} + const canThen: CanThen = Foo.resolve(2); + + canThen; + canThen.then(() => {}); + void canThen.catch(); + canThen.finally(); +} + `, + }, + ], }, { line: 9, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + class CanThen extends Promise {} + const canThen: CanThen = Foo.resolve(2); + + canThen; + canThen.then(() => {}); + canThen.catch(); + void canThen.finally(); +} + `, + }, + ], }, ], }, @@ -1306,10 +2006,46 @@ async function test() { { line: 10, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + class CatchableThenable { + then(callback: () => void, callback: () => void): CatchableThenable { + return new CatchableThenable(); + } + } + const thenable = new CatchableThenable(); + + void thenable; + thenable.then(() => {}); +} + `, + }, + ], }, { line: 11, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + class CatchableThenable { + then(callback: () => void, callback: () => void): CatchableThenable { + return new CatchableThenable(); + } + } + const thenable = new CatchableThenable(); + + thenable; + void thenable.then(() => {}); +} + `, + }, + ], }, ], }, @@ -1340,31 +2076,122 @@ async function test() { { line: 18, messageId: 'floatingVoid', - }, - { - line: 19, - messageId: 'floatingVoid', - }, - { - line: 20, - messageId: 'floatingVoid', - }, - ], - }, - { - code: ` - (async () => { - await something(); - })(); - `, - errors: [ - { - line: 2, - messageId: 'floatingVoid', - }, - ], - }, - { + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/promise-polyfill/index.d.ts +// Type definitions for promise-polyfill 6.0 +// Project: https://github.com/taylorhakes/promise-polyfill +// Definitions by: Steve Jenkins +// Daniel Cassidy +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +interface PromisePolyfillConstructor extends PromiseConstructor { + _immediateFn?: (handler: (() => void) | string) => void; +} + +declare const PromisePolyfill: PromisePolyfillConstructor; + +async function test() { + const promise = new PromisePolyfill(() => {}); + + void promise; + promise.then(() => {}); + promise.catch(); +} + `, + }, + ], + }, + { + line: 19, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/promise-polyfill/index.d.ts +// Type definitions for promise-polyfill 6.0 +// Project: https://github.com/taylorhakes/promise-polyfill +// Definitions by: Steve Jenkins +// Daniel Cassidy +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +interface PromisePolyfillConstructor extends PromiseConstructor { + _immediateFn?: (handler: (() => void) | string) => void; +} + +declare const PromisePolyfill: PromisePolyfillConstructor; + +async function test() { + const promise = new PromisePolyfill(() => {}); + + promise; + void promise.then(() => {}); + promise.catch(); +} + `, + }, + ], + }, + { + line: 20, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/promise-polyfill/index.d.ts +// Type definitions for promise-polyfill 6.0 +// Project: https://github.com/taylorhakes/promise-polyfill +// Definitions by: Steve Jenkins +// Daniel Cassidy +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +interface PromisePolyfillConstructor extends PromiseConstructor { + _immediateFn?: (handler: (() => void) | string) => void; +} + +declare const PromisePolyfill: PromisePolyfillConstructor; + +async function test() { + const promise = new PromisePolyfill(() => {}); + + promise; + promise.then(() => {}); + void promise.catch(); +} + `, + }, + ], + }, + ], + }, + { + code: ` + (async () => { + await something(); + })(); + `, + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + void (async () => { + await something(); + })(); + `, + }, + ], + }, + ], + }, + { code: ` (async () => { something(); @@ -1374,6 +2201,16 @@ async function test() { { line: 2, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + void (async () => { + something(); + })(); + `, + }, + ], }, ], }, @@ -1383,6 +2220,12 @@ async function test() { { line: 1, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: 'void (async function foo() {})();', + }, + ], }, ], }, @@ -1396,6 +2239,16 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + function foo() { + void (async function bar() {})(); + } + `, + }, + ], }, ], }, @@ -1412,6 +2265,19 @@ async function test() { { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + const foo = () => + new Promise(res => { + void (async function () { + await res(1); + })(); + }); + `, + }, + ], }, ], }, @@ -1425,6 +2291,16 @@ async function test() { { line: 2, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + void (async function () { + await res(1); + })(); + `, + }, + ], }, ], }, @@ -1439,6 +2315,16 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + (async function () { + void Promise.resolve(); + })(); + `, + }, + ], }, ], }, @@ -1457,18 +2343,74 @@ async function test() { { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + (async function () { + declare const promiseIntersection: Promise & number; + void promiseIntersection; + promiseIntersection.then(() => {}); + promiseIntersection.catch(); + promiseIntersection.finally(); + })(); + `, + }, + ], }, { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + (async function () { + declare const promiseIntersection: Promise & number; + promiseIntersection; + void promiseIntersection.then(() => {}); + promiseIntersection.catch(); + promiseIntersection.finally(); + })(); + `, + }, + ], }, { line: 6, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + (async function () { + declare const promiseIntersection: Promise & number; + promiseIntersection; + promiseIntersection.then(() => {}); + void promiseIntersection.catch(); + promiseIntersection.finally(); + })(); + `, + }, + ], }, { line: 7, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + (async function () { + declare const promiseIntersection: Promise & number; + promiseIntersection; + promiseIntersection.then(() => {}); + promiseIntersection.catch(); + void promiseIntersection.finally(); + })(); + `, + }, + ], }, ], }, @@ -1540,49 +2482,402 @@ async function foo() { condition && myPromise(); } `, - errors: [ + errors: [ + { + line: 6, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function foo() { + const myPromise = async () => void 0; + const condition = true; + + void (condition && myPromise()); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = false; + + condition || myPromise(); +} + `, + errors: [ + { + line: 6, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function foo() { + const myPromise = async () => void 0; + const condition = false; + + void (condition || myPromise()); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = null; + + condition ?? myPromise(); +} + `, + errors: [ + { + line: 6, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function foo() { + const myPromise = async () => void 0; + const condition = null; + + void (condition ?? myPromise()); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = Promise.resolve(true); + let condition = true; + condition && myPromise; +} + `, + options: [{ ignoreVoid: false }], + errors: [ + { + line: 5, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function foo() { + const myPromise = Promise.resolve(true); + let condition = true; + await (condition && myPromise); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = Promise.resolve(true); + let condition = false; + condition || myPromise; +} + `, + options: [{ ignoreVoid: false }], + errors: [ + { + line: 5, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function foo() { + const myPromise = Promise.resolve(true); + let condition = false; + await (condition || myPromise); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = Promise.resolve(true); + let condition = null; + condition ?? myPromise; +} + `, + options: [{ ignoreVoid: false }], + errors: [ + { + line: 5, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +async function foo() { + const myPromise = Promise.resolve(true); + let condition = null; + await (condition ?? myPromise); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function foo() { + const myPromise = async () => void 0; + const condition = false; + + condition || condition || myPromise(); +} + `, + errors: [ + { + line: 6, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function foo() { + const myPromise = async () => void 0; + const condition = false; + + void (condition || condition || myPromise()); +} + `, + }, + ], + }, + ], + }, + { + code: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + errors: [ + { + line: 4, + messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +void Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], + }, + { + line: 5, + messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +void Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], + }, + { + line: 6, + messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +void Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], + }, + { + line: 7, + messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +void Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], + }, + { + line: 10, + messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +void Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], + }, + { + line: 11, + messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +void Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], + }, { - line: 6, - messageId: 'floatingVoid', + line: 12, + messageId: 'floatingUselessRejectionHandlerVoid', suggestions: [ { messageId: 'floatingFixVoid', output: ` -async function foo() { - const myPromise = async () => void 0; - const condition = true; +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); - void (condition && myPromise()); -} +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +void Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); `, }, ], }, - ], - }, - { - code: ` -async function foo() { - const myPromise = async () => void 0; - const condition = false; - - condition || myPromise(); -} - `, - errors: [ { - line: 6, - messageId: 'floatingVoid', + line: 13, + messageId: 'floatingUselessRejectionHandlerVoid', suggestions: [ { messageId: 'floatingFixVoid', output: ` -async function foo() { - const myPromise = async () => void 0; - const condition = false; +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); - void (condition || myPromise()); -} +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +void Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); `, }, ], @@ -1591,27 +2886,17 @@ async function foo() { }, { code: ` -async function foo() { - const myPromise = async () => void 0; - const condition = null; - - condition ?? myPromise(); -} +Promise.reject() || 3; `, errors: [ { - line: 6, + line: 2, messageId: 'floatingVoid', suggestions: [ { messageId: 'floatingFixVoid', output: ` -async function foo() { - const myPromise = async () => void 0; - const condition = null; - - void (condition ?? myPromise()); -} +void (Promise.reject() || 3); `, }, ], @@ -1620,26 +2905,18 @@ async function foo() { }, { code: ` -async function foo() { - const myPromise = Promise.resolve(true); - let condition = true; - condition && myPromise; -} +void Promise.resolve().then(() => {}, undefined); `, options: [{ ignoreVoid: false }], errors: [ { - line: 5, - messageId: 'floating', + line: 2, + messageId: 'floatingUselessRejectionHandler', suggestions: [ { messageId: 'floatingFixAwait', output: ` -async function foo() { - const myPromise = Promise.resolve(true); - let condition = true; - await (condition && myPromise); -} +await Promise.resolve().then(() => {}, undefined); `, }, ], @@ -1648,26 +2925,20 @@ async function foo() { }, { code: ` -async function foo() { - const myPromise = Promise.resolve(true); - let condition = false; - condition || myPromise; -} +declare const maybeCallable: string | (() => void); +Promise.resolve().then(() => {}, maybeCallable); `, options: [{ ignoreVoid: false }], errors: [ { - line: 5, - messageId: 'floating', + line: 3, + messageId: 'floatingUselessRejectionHandler', suggestions: [ { messageId: 'floatingFixAwait', output: ` -async function foo() { - const myPromise = Promise.resolve(true); - let condition = false; - await (condition || myPromise); -} +declare const maybeCallable: string | (() => void); +await Promise.resolve().then(() => {}, maybeCallable); `, }, ], @@ -1676,67 +2947,57 @@ async function foo() { }, { code: ` -async function foo() { - const myPromise = Promise.resolve(true); - let condition = null; - condition ?? myPromise; -} +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); `, options: [{ ignoreVoid: false }], errors: [ { - line: 5, - messageId: 'floating', + line: 4, + messageId: 'floatingUselessRejectionHandler', suggestions: [ { messageId: 'floatingFixAwait', output: ` -async function foo() { - const myPromise = Promise.resolve(true); - let condition = null; - await (condition ?? myPromise); -} +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +await Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); `, }, ], }, - ], - }, - { - code: ` -async function foo() { - const myPromise = async () => void 0; - const condition = false; - - condition || condition || myPromise(); -} - `, - errors: [ { - line: 6, - messageId: 'floatingVoid', + line: 5, + messageId: 'floatingUselessRejectionHandler', suggestions: [ { - messageId: 'floatingFixVoid', + messageId: 'floatingFixAwait', output: ` -async function foo() { - const myPromise = async () => void 0; - const condition = false; - - void (condition || condition || myPromise()); -} - `, - }, - ], - }, - ], - }, - { - code: ` declare const maybeCallable: string | (() => void); declare const definitelyCallable: () => void; Promise.resolve().then(() => {}, undefined); -Promise.resolve().then(() => {}, null); +await Promise.resolve().then(() => {}, null); Promise.resolve().then(() => {}, 3); Promise.resolve().then(() => {}, maybeCallable); Promise.resolve().then(() => {}, definitelyCallable); @@ -1747,85 +3008,46 @@ Promise.resolve().catch(3); Promise.resolve().catch(maybeCallable); Promise.resolve().catch(definitelyCallable); `, - errors: [ - { - line: 4, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - { - line: 5, - messageId: 'floatingUselessRejectionHandlerVoid', + }, + ], }, { line: 6, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - { - line: 7, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - { - line: 10, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - { - line: 11, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - { - line: 12, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - { - line: 13, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - ], - }, - { - code: ` -Promise.reject() || 3; - `, - errors: [ - { - line: 2, - messageId: 'floatingVoid', - }, - ], - }, - { - code: ` -void Promise.resolve().then(() => {}, undefined); - `, - options: [{ ignoreVoid: false }], - errors: [ - { - line: 2, messageId: 'floatingUselessRejectionHandler', - }, - ], - }, - { - code: ` + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +await Promise.resolve().then(() => {}, 3); Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); `, - options: [{ ignoreVoid: false }], - errors: [ + }, + ], + }, { - line: 3, + line: 7, messageId: 'floatingUselessRejectionHandler', - }, - ], - }, - { - code: ` + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` declare const maybeCallable: string | (() => void); declare const definitelyCallable: () => void; Promise.resolve().then(() => {}, undefined); Promise.resolve().then(() => {}, null); Promise.resolve().then(() => {}, 3); -Promise.resolve().then(() => {}, maybeCallable); +await Promise.resolve().then(() => {}, maybeCallable); Promise.resolve().then(() => {}, definitelyCallable); Promise.resolve().catch(undefined); @@ -1834,39 +3056,104 @@ Promise.resolve().catch(3); Promise.resolve().catch(maybeCallable); Promise.resolve().catch(definitelyCallable); `, - options: [{ ignoreVoid: false }], - errors: [ - { - line: 4, - messageId: 'floatingUselessRejectionHandler', - }, - { - line: 5, - messageId: 'floatingUselessRejectionHandler', - }, - { - line: 6, - messageId: 'floatingUselessRejectionHandler', - }, - { - line: 7, - messageId: 'floatingUselessRejectionHandler', + }, + ], }, { line: 10, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +await Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, { line: 11, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +await Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, { line: 12, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +await Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, { line: 13, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +await Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, ], }, @@ -1879,6 +3166,14 @@ Promise.reject() || 3; { line: 2, messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +await (Promise.reject() || 3); + `, + }, + ], }, ], }, @@ -1886,7 +3181,20 @@ Promise.reject() || 3; code: ` Promise.reject().finally(() => {}); `, - errors: [{ line: 2, messageId: 'floatingVoid' }], + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void Promise.reject().finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` @@ -1895,7 +3203,22 @@ Promise.reject() .finally(() => {}); `, options: [{ ignoreVoid: false }], - errors: [{ line: 2, messageId: 'floating' }], + errors: [ + { + line: 2, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +await Promise.reject() + .finally(() => {}) + .finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` @@ -1904,7 +3227,23 @@ Promise.reject() .finally(() => {}) .finally(() => {}); `, - errors: [{ line: 2, messageId: 'floatingVoid' }], + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void Promise.reject() + .finally(() => {}) + .finally(() => {}) + .finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` @@ -1912,39 +3251,121 @@ Promise.reject() .then(() => {}) .finally(() => {}); `, - errors: [{ line: 2, messageId: 'floatingVoid' }], + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void Promise.reject() + .then(() => {}) + .finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` declare const returnsPromise: () => Promise | null; returnsPromise()?.finally(() => {}); `, - errors: [{ line: 3, messageId: 'floatingVoid' }], + errors: [ + { + line: 3, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const returnsPromise: () => Promise | null; +void returnsPromise()?.finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` const promiseIntersection: Promise & number; promiseIntersection.finally(() => {}); `, - errors: [{ line: 3, messageId: 'floatingVoid' }], + errors: [ + { + line: 3, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const promiseIntersection: Promise & number; +void promiseIntersection.finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` Promise.resolve().finally(() => {}), 123; `, - errors: [{ line: 2, messageId: 'floatingVoid' }], + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void (Promise.resolve().finally(() => {}), 123); + `, + }, + ], + }, + ], }, { code: ` (async () => true)().finally(); `, - errors: [{ line: 2, messageId: 'floatingVoid' }], + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void (async () => true)().finally(); + `, + }, + ], + }, + ], }, { code: ` Promise.reject(new Error('message')).finally(() => {}); `, - errors: [{ line: 2, messageId: 'floatingVoid' }], + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void Promise.reject(new Error('message')).finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` @@ -1954,7 +3375,24 @@ function _>>( maybePromiseArray?.[0]; } `, - errors: [{ line: 5, messageId: 'floatingVoid' }], + errors: [ + { + line: 5, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +function _>>( + maybePromiseArray: S | undefined, +): void { + void maybePromiseArray?.[0]; +} + `, + }, + ], + }, + ], }, { code: ` diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts index b2d4eb907b33..9a7f49dc3ecc 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts @@ -409,7 +409,23 @@ ruleTester.run('strict-enums-comparison', rule, { } Fruit.Apple === 0; `, - errors: [{ messageId: 'mismatchedCondition' }], + errors: [ + { + messageId: 'mismatchedCondition', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Fruit { + Apple = 0, + Banana = 'banana', + } + Fruit.Apple === Fruit.Apple; + `, + }, + ], + }, + ], }, { code: ` @@ -584,10 +600,126 @@ ruleTester.run('strict-enums-comparison', rule, { mixed === 1; `, errors: [ - { messageId: 'mismatchedCondition' }, - { messageId: 'mismatchedCondition' }, - { messageId: 'mismatchedCondition' }, - { messageId: 'mismatchedCondition' }, + { + messageId: 'mismatchedCondition', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Str { + A = 'a', + } + enum Num { + B = 1, + } + enum Mixed { + A = 'a', + B = 1, + } + + declare const str: Str; + declare const num: Num; + declare const mixed: Mixed; + + // following are all errors because the value might be an enum value + str === Str.A; + num === 1; + mixed === 'a'; + mixed === 1; + `, + }, + ], + }, + { + messageId: 'mismatchedCondition', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Str { + A = 'a', + } + enum Num { + B = 1, + } + enum Mixed { + A = 'a', + B = 1, + } + + declare const str: Str; + declare const num: Num; + declare const mixed: Mixed; + + // following are all errors because the value might be an enum value + str === 'a'; + num === Num.B; + mixed === 'a'; + mixed === 1; + `, + }, + ], + }, + { + messageId: 'mismatchedCondition', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Str { + A = 'a', + } + enum Num { + B = 1, + } + enum Mixed { + A = 'a', + B = 1, + } + + declare const str: Str; + declare const num: Num; + declare const mixed: Mixed; + + // following are all errors because the value might be an enum value + str === 'a'; + num === 1; + mixed === Mixed.A; + mixed === 1; + `, + }, + ], + }, + { + messageId: 'mismatchedCondition', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Str { + A = 'a', + } + enum Num { + B = 1, + } + enum Mixed { + A = 'a', + B = 1, + } + + declare const str: Str; + declare const num: Num; + declare const mixed: Mixed; + + // following are all errors because the value might be an enum value + str === 'a'; + num === 1; + mixed === 'a'; + mixed === Mixed.B; + `, + }, + ], + }, ], }, { diff --git a/packages/eslint-plugin/tests/rules/prefer-as-const.test.ts b/packages/eslint-plugin/tests/rules/prefer-as-const.test.ts index 19058d692b42..e0c917c88f21 100644 --- a/packages/eslint-plugin/tests/rules/prefer-as-const.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-as-const.test.ts @@ -121,6 +121,12 @@ ruleTester.run('prefer-as-const', rule, { messageId: 'variableConstAssertion', line: 1, column: 9, + suggestions: [ + { + messageId: 'variableSuggest', + output: "let [] = 'bar' as const;", + }, + ], }, ], }, diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 5affe9874a50..e3547bb82591 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -957,7 +957,20 @@ x || y; ignorePrimitives: { number: true, boolean: true, bigint: true }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: string | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -970,7 +983,20 @@ x || y; ignorePrimitives: { string: true, boolean: true, bigint: true }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: number | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -983,7 +1009,20 @@ x || y; ignorePrimitives: { string: true, number: true, bigint: true }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: boolean | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -996,7 +1035,20 @@ x || y; ignorePrimitives: { string: true, number: true, boolean: true }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: bigint | undefined; +x ?? y; + `, + }, + ], + }, + ], }, // falsy { @@ -1015,7 +1067,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: '' | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1033,7 +1098,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: \`\` | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1051,7 +1129,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 0 | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1069,7 +1160,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 0n | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1087,7 +1191,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: false | undefined; +x ?? y; + `, + }, + ], + }, + ], }, // truthy { @@ -1106,7 +1223,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 'a' | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1124,7 +1254,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: \`hello\${'string'}\` | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1142,7 +1285,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 1 | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1160,7 +1316,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 1n | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1178,7 +1347,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: true | undefined; +x ?? y; + `, + }, + ], + }, + ], }, // Unions of same primitive { @@ -1197,7 +1379,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 'a' | 'b' | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1215,7 +1410,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 'a' | \`b\` | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1233,7 +1441,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 0 | 1 | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1251,7 +1472,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 1 | 2 | 3 | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1269,7 +1503,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 0n | 1n | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1287,7 +1534,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 1n | 2n | 3n | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1305,7 +1565,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: true | false | undefined; +x ?? y; + `, + }, + ], + }, + ], }, // Mixed unions { @@ -1324,7 +1597,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 0 | 1 | 0n | 1n | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1342,7 +1628,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: true | false | null | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1353,6 +1652,15 @@ x || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 0 | 'foo' | undefined; +x ?? y; + `, + }, + ], }, ], }, @@ -1387,6 +1695,15 @@ undefined || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 0 | 'foo' | undefined; +x ?? y; + `, + }, + ], }, ], }, @@ -1404,6 +1721,15 @@ x || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: null; +x ?? y; + `, + }, + ], }, ], }, @@ -1421,6 +1747,15 @@ x || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +const x = undefined; +x ?? y; + `, + }, + ], }, ], }, @@ -1438,6 +1773,14 @@ x || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +null ?? y; + `, + }, + ], }, ], }, @@ -1455,6 +1798,14 @@ x || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +undefined ?? y; + `, + }, + ], }, ], }, diff --git a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts index bd25e35fe136..dd1bb0a92a38 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -523,9 +523,49 @@ if (y) { code: "'asd' && 123 && [] && null;", output: null, errors: [ - { messageId: 'conditionErrorString', line: 1, column: 1 }, - { messageId: 'conditionErrorNumber', line: 1, column: 10 }, - { messageId: 'conditionErrorObject', line: 1, column: 17 }, + { + messageId: 'conditionErrorString', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: "('asd'.length > 0) && 123 && [] && null;", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: '(\'asd\' !== "") && 123 && [] && null;', + }, + { + messageId: 'conditionFixCastBoolean', + output: "(Boolean('asd')) && 123 && [] && null;", + }, + ], + }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 10, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "'asd' && (123 !== 0) && [] && null;", + }, + { + messageId: 'conditionFixCompareNaN', + output: "'asd' && (!Number.isNaN(123)) && [] && null;", + }, + { + messageId: 'conditionFixCastBoolean', + output: "'asd' && (Boolean(123)) && [] && null;", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 17, + }, ], }, { @@ -533,9 +573,49 @@ if (y) { code: "'asd' || 123 || [] || null;", output: null, errors: [ - { messageId: 'conditionErrorString', line: 1, column: 1 }, - { messageId: 'conditionErrorNumber', line: 1, column: 10 }, - { messageId: 'conditionErrorObject', line: 1, column: 17 }, + { + messageId: 'conditionErrorString', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: "('asd'.length > 0) || 123 || [] || null;", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: '(\'asd\' !== "") || 123 || [] || null;', + }, + { + messageId: 'conditionFixCastBoolean', + output: "(Boolean('asd')) || 123 || [] || null;", + }, + ], + }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 10, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "'asd' || (123 !== 0) || [] || null;", + }, + { + messageId: 'conditionFixCompareNaN', + output: "'asd' || (!Number.isNaN(123)) || [] || null;", + }, + { + messageId: 'conditionFixCastBoolean', + output: "'asd' || (Boolean(123)) || [] || null;", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 17, + }, ], }, { @@ -543,11 +623,91 @@ if (y) { code: "let x = (1 && 'a' && null) || 0 || '' || {};", output: null, errors: [ - { messageId: 'conditionErrorNumber', line: 1, column: 10 }, - { messageId: 'conditionErrorString', line: 1, column: 15 }, - { messageId: 'conditionErrorNullish', line: 1, column: 22 }, - { messageId: 'conditionErrorNumber', line: 1, column: 31 }, - { messageId: 'conditionErrorString', line: 1, column: 36 }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 10, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "let x = ((1 !== 0) && 'a' && null) || 0 || '' || {};", + }, + { + messageId: 'conditionFixCompareNaN', + output: + "let x = ((!Number.isNaN(1)) && 'a' && null) || 0 || '' || {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "let x = ((Boolean(1)) && 'a' && null) || 0 || '' || {};", + }, + ], + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 15, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: + "let x = (1 && ('a'.length > 0) && null) || 0 || '' || {};", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: "let x = (1 && ('a' !== \"\") && null) || 0 || '' || {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "let x = (1 && (Boolean('a')) && null) || 0 || '' || {};", + }, + ], + }, + { + messageId: 'conditionErrorNullish', + line: 1, + column: 22, + }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 31, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "let x = (1 && 'a' && null) || (0 !== 0) || '' || {};", + }, + { + messageId: 'conditionFixCompareNaN', + output: + "let x = (1 && 'a' && null) || (!Number.isNaN(0)) || '' || {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "let x = (1 && 'a' && null) || (Boolean(0)) || '' || {};", + }, + ], + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 36, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: + "let x = (1 && 'a' && null) || 0 || (''.length > 0) || {};", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: "let x = (1 && 'a' && null) || 0 || ('' !== \"\") || {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "let x = (1 && 'a' && null) || 0 || (Boolean('')) || {};", + }, + ], + }, ], }, { @@ -555,11 +715,91 @@ if (y) { code: "return (1 || 'a' || null) && 0 && '' && {};", output: null, errors: [ - { messageId: 'conditionErrorNumber', line: 1, column: 9 }, - { messageId: 'conditionErrorString', line: 1, column: 14 }, - { messageId: 'conditionErrorNullish', line: 1, column: 21 }, - { messageId: 'conditionErrorNumber', line: 1, column: 30 }, - { messageId: 'conditionErrorString', line: 1, column: 35 }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 9, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "return ((1 !== 0) || 'a' || null) && 0 && '' && {};", + }, + { + messageId: 'conditionFixCompareNaN', + output: + "return ((!Number.isNaN(1)) || 'a' || null) && 0 && '' && {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "return ((Boolean(1)) || 'a' || null) && 0 && '' && {};", + }, + ], + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 14, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: + "return (1 || ('a'.length > 0) || null) && 0 && '' && {};", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: "return (1 || ('a' !== \"\") || null) && 0 && '' && {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "return (1 || (Boolean('a')) || null) && 0 && '' && {};", + }, + ], + }, + { + messageId: 'conditionErrorNullish', + line: 1, + column: 21, + }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 30, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "return (1 || 'a' || null) && (0 !== 0) && '' && {};", + }, + { + messageId: 'conditionFixCompareNaN', + output: + "return (1 || 'a' || null) && (!Number.isNaN(0)) && '' && {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "return (1 || 'a' || null) && (Boolean(0)) && '' && {};", + }, + ], + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 35, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: + "return (1 || 'a' || null) && 0 && (''.length > 0) && {};", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: "return (1 || 'a' || null) && 0 && ('' !== \"\") && {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "return (1 || 'a' || null) && 0 && (Boolean('')) && {};", + }, + ], + }, ], }, { @@ -567,9 +807,49 @@ if (y) { code: "console.log((1 && []) || ('a' && {}));", output: null, errors: [ - { messageId: 'conditionErrorNumber', line: 1, column: 14 }, - { messageId: 'conditionErrorObject', line: 1, column: 19 }, - { messageId: 'conditionErrorString', line: 1, column: 27 }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 14, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "console.log(((1 !== 0) && []) || ('a' && {}));", + }, + { + messageId: 'conditionFixCompareNaN', + output: "console.log(((!Number.isNaN(1)) && []) || ('a' && {}));", + }, + { + messageId: 'conditionFixCastBoolean', + output: "console.log(((Boolean(1)) && []) || ('a' && {}));", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 19, + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 27, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: "console.log((1 && []) || (('a'.length > 0) && {}));", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: 'console.log((1 && []) || ((\'a\' !== "") && {}));', + }, + { + messageId: 'conditionFixCastBoolean', + output: "console.log((1 && []) || ((Boolean('a')) && {}));", + }, + ], + }, ], }, @@ -579,10 +859,54 @@ if (y) { code: "if ((1 && []) || ('a' && {})) void 0;", output: null, errors: [ - { messageId: 'conditionErrorNumber', line: 1, column: 6 }, - { messageId: 'conditionErrorObject', line: 1, column: 11 }, - { messageId: 'conditionErrorString', line: 1, column: 19 }, - { messageId: 'conditionErrorObject', line: 1, column: 26 }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 6, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "if (((1 !== 0) && []) || ('a' && {})) void 0;", + }, + { + messageId: 'conditionFixCompareNaN', + output: "if (((!Number.isNaN(1)) && []) || ('a' && {})) void 0;", + }, + { + messageId: 'conditionFixCastBoolean', + output: "if (((Boolean(1)) && []) || ('a' && {})) void 0;", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 11, + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 19, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: "if ((1 && []) || (('a'.length > 0) && {})) void 0;", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: 'if ((1 && []) || ((\'a\' !== "") && {})) void 0;', + }, + { + messageId: 'conditionFixCastBoolean', + output: "if ((1 && []) || ((Boolean('a')) && {})) void 0;", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 26, + }, ], }, { @@ -590,10 +914,60 @@ if (y) { code: "let x = null || 0 || 'a' || [] ? {} : undefined;", output: null, errors: [ - { messageId: 'conditionErrorNullish', line: 1, column: 9 }, - { messageId: 'conditionErrorNumber', line: 1, column: 17 }, - { messageId: 'conditionErrorString', line: 1, column: 22 }, - { messageId: 'conditionErrorObject', line: 1, column: 29 }, + { + messageId: 'conditionErrorNullish', + line: 1, + column: 9, + }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 17, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: + "let x = null || (0 !== 0) || 'a' || [] ? {} : undefined;", + }, + { + messageId: 'conditionFixCompareNaN', + output: + "let x = null || (!Number.isNaN(0)) || 'a' || [] ? {} : undefined;", + }, + { + messageId: 'conditionFixCastBoolean', + output: + "let x = null || (Boolean(0)) || 'a' || [] ? {} : undefined;", + }, + ], + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 22, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: + "let x = null || 0 || ('a'.length > 0) || [] ? {} : undefined;", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: + 'let x = null || 0 || (\'a\' !== "") || [] ? {} : undefined;', + }, + { + messageId: 'conditionFixCastBoolean', + output: + "let x = null || 0 || (Boolean('a')) || [] ? {} : undefined;", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 29, + }, ], }, { @@ -601,10 +975,54 @@ if (y) { code: "return !(null || 0 || 'a' || []);", output: null, errors: [ - { messageId: 'conditionErrorNullish', line: 1, column: 10 }, - { messageId: 'conditionErrorNumber', line: 1, column: 18 }, - { messageId: 'conditionErrorString', line: 1, column: 23 }, - { messageId: 'conditionErrorObject', line: 1, column: 30 }, + { + messageId: 'conditionErrorNullish', + line: 1, + column: 10, + }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 18, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "return !(null || (0 !== 0) || 'a' || []);", + }, + { + messageId: 'conditionFixCompareNaN', + output: "return !(null || (!Number.isNaN(0)) || 'a' || []);", + }, + { + messageId: 'conditionFixCastBoolean', + output: "return !(null || (Boolean(0)) || 'a' || []);", + }, + ], + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 23, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: "return !(null || 0 || ('a'.length > 0) || []);", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: 'return !(null || 0 || (\'a\' !== "") || []);', + }, + { + messageId: 'conditionFixCastBoolean', + output: "return !(null || 0 || (Boolean('a')) || []);", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 30, + }, ], }, From 8c3f96ca69a55720e76dc5ed0bb31c4fcb65a24b Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Mon, 13 May 2024 16:52:04 -0400 Subject: [PATCH 07/17] Implemented all missing suggestions --- .../rules/switch-exhaustiveness-check.test.ts | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts index ff4e2a4199db..792533c9c2b3 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -1561,6 +1561,37 @@ switch (day) { missingBranches: '"Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +type Day = + | 'Monday' + | 'Tuesday' + | 'Wednesday' + | 'Thursday' + | 'Friday' + | 'Saturday' + | 'Sunday'; + +const day = 'Monday' as Day; +let result = 0; + +switch (day) { + case 'Monday': { + result = 1; + break; + } + case "Tuesday": { throw new Error('Not implemented yet: "Tuesday" case') } + case "Wednesday": { throw new Error('Not implemented yet: "Wednesday" case') } + case "Thursday": { throw new Error('Not implemented yet: "Thursday" case') } + case "Friday": { throw new Error('Not implemented yet: "Friday" case') } + case "Saturday": { throw new Error('Not implemented yet: "Saturday" case') } + case "Sunday": { throw new Error('Not implemented yet: "Sunday" case') } +} + `, + }, + ], }, ], }, @@ -1587,6 +1618,25 @@ function test(value: Enum): number { data: { missingBranches: 'Enum.B', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +enum Enum { + A, + B, +} + +function test(value: Enum): number { + switch (value) { + case Enum.A: + return 1; + case Enum.B: { throw new Error('Not implemented yet: Enum.B case') } + } +} + `, + }, + ], }, ], }, @@ -1612,6 +1662,26 @@ function test(value: Union): number { data: { missingBranches: '"b" | "c"', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +type A = 'a'; +type B = 'b'; +type C = 'c'; +type Union = A | B | C; + +function test(value: Union): number { + switch (value) { + case 'a': + return 1; + case "b": { throw new Error('Not implemented yet: "b" case') } + case "c": { throw new Error('Not implemented yet: "c" case') } + } +} + `, + }, + ], }, ], }, @@ -1638,6 +1708,27 @@ function test(value: Union): number { data: { missingBranches: 'true | 1', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +const A = 'a'; +const B = 1; +const C = true; + +type Union = typeof A | typeof B | typeof C; + +function test(value: Union): number { + switch (value) { + case 'a': + return 1; + case true: { throw new Error('Not implemented yet: true case') } + case 1: { throw new Error('Not implemented yet: 1 case') } + } +} + `, + }, + ], }, ], }, @@ -1660,6 +1751,22 @@ function test(value: DiscriminatedUnion): number { data: { missingBranches: '"B"', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +type DiscriminatedUnion = { type: 'A'; a: 1 } | { type: 'B'; b: 2 }; + +function test(value: DiscriminatedUnion): number { + switch (value.type) { + case 'A': + return 1; + case "B": { throw new Error('Not implemented yet: "B" case') } + } +} + `, + }, + ], }, ], }, @@ -1689,6 +1796,33 @@ switch (day) { missingBranches: '"Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +type Day = + | 'Monday' + | 'Tuesday' + | 'Wednesday' + | 'Thursday' + | 'Friday' + | 'Saturday' + | 'Sunday'; + +const day = 'Monday' as Day; + +switch (day) { +case "Monday": { throw new Error('Not implemented yet: "Monday" case') } +case "Tuesday": { throw new Error('Not implemented yet: "Tuesday" case') } +case "Wednesday": { throw new Error('Not implemented yet: "Wednesday" case') } +case "Thursday": { throw new Error('Not implemented yet: "Thursday" case') } +case "Friday": { throw new Error('Not implemented yet: "Friday" case') } +case "Saturday": { throw new Error('Not implemented yet: "Saturday" case') } +case "Sunday": { throw new Error('Not implemented yet: "Sunday" case') } +} + `, + }, + ], }, ], }, @@ -1715,6 +1849,27 @@ function test(value: T): number { data: { missingBranches: 'typeof b | typeof c', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +const a = Symbol('a'); +const b = Symbol('b'); +const c = Symbol('c'); + +type T = typeof a | typeof b | typeof c; + +function test(value: T): number { + switch (value) { + case a: + return 1; + case b: { throw new Error('Not implemented yet: b case') } + case c: { throw new Error('Not implemented yet: c case') } + } +} + `, + }, + ], }, ], }, From bac7183c98d6b3fa6be1566553012b08fb364d42 Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Mon, 13 May 2024 17:15:00 -0400 Subject: [PATCH 08/17] Sent expectedDesc back inside the if (hasOwnProperty(expectedSuggestion, 'desc')) verification --- packages/rule-tester/src/RuleTester.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 7f3c69a84af8..86c693a42937 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -1009,8 +1009,6 @@ export class RuleTester extends TestFramework { expectedSuggestion != null, "Test suggestion in 'suggestions' array must be an object.", ); - // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` - const expectedDesc = expectedSuggestion.desc as string; Object.keys(expectedSuggestion).forEach(propertyName => { assert.ok( SUGGESTION_OBJECT_PARAMETERS.has(propertyName), @@ -1035,6 +1033,9 @@ export class RuleTester extends TestFramework { // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` if (hasOwnProperty(expectedSuggestion, 'desc')) { + // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` + const expectedDesc = expectedSuggestion.desc as string; + assert.ok( !hasOwnProperty(expectedSuggestion, 'data'), `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`, From 231543755d056101e507f62bdbbab0b820209f5a Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Thu, 30 May 2024 12:27:01 -0400 Subject: [PATCH 09/17] Fixed unsubstitutedPlaceholders placement --- packages/rule-tester/src/RuleTester.ts | 37 +++++++++----------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 86c693a42937..65e7bfc3d61a 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -886,18 +886,6 @@ export class RuleTester extends TestFramework { `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`, ); - const unsubstitutedPlaceholders = - getUnsubstitutedMessagePlaceholders( - message.message, - rule.meta.messages[message.messageId], - error.data, - ); - - assert.ok( - unsubstitutedPlaceholders.length === 0, - `The reported message has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(', ')}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? 'values' : 'value'} via the 'data' property in the context.report() call.`, - ); - if (hasOwnProperty(error, 'data')) { /* * if data was provided, then directly compare the returned message to a synthetic @@ -1019,18 +1007,6 @@ export class RuleTester extends TestFramework { const actualSuggestion = messageSuggestions[index]; const suggestionPrefix = `Error Suggestion at index ${index}:`; - const unsubstitutedPlaceholders = - getUnsubstitutedMessagePlaceholders( - actualSuggestion.desc, - rule.meta.messages[expectedSuggestion.messageId], - expectedSuggestion.data, - ); - - assert.ok( - unsubstitutedPlaceholders.length === 0, - `The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(', ')}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? 'values' : 'value'} via the 'data' property for the suggestion in the context.report() call.`, - ); - // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` if (hasOwnProperty(expectedSuggestion, 'desc')) { // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` @@ -1068,6 +1044,19 @@ export class RuleTester extends TestFramework { expectedSuggestion.messageId, `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`, ); + + const unsubstitutedPlaceholders = + getUnsubstitutedMessagePlaceholders( + actualSuggestion.desc, + rule.meta.messages[expectedSuggestion.messageId], + expectedSuggestion.data, + ); + + assert.ok( + unsubstitutedPlaceholders.length === 0, + `The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(', ')}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? 'values' : 'value'} via the 'data' property for the suggestion in the context.report() call.`, + ); + if (hasOwnProperty(expectedSuggestion, 'data')) { const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId]; From 09bfd382a2baf26bf021c3b112fa84249b720b93 Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Mon, 3 Jun 2024 09:58:57 -0400 Subject: [PATCH 10/17] Fixed errors --- .../tests/rules/no-floating-promises.test.ts | 113 +++++++++++++++++- 1 file changed, 107 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts index 308e1ebbe4a6..4911e944b64d 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -3532,7 +3532,33 @@ promise; allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }], }, ], - errors: [{ line: 15, messageId: 'floatingVoid' }], + errors: [ + { + line: 15, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +interface UnsafeThenable { + then( + onfulfilled?: + | ((value: T) => TResult1 | UnsafeThenable) + | undefined + | null, + onrejected?: + | ((reason: any) => TResult2 | UnsafeThenable) + | undefined + | null, + ): UnsafeThenable; +} +let promise: UnsafeThenable = Promise.resolve(5); +void promise; + `, + }, + ], + }, + ], }, { code: ` @@ -3543,7 +3569,22 @@ promise.catch(); options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, ], - errors: [{ line: 4, messageId: 'floatingVoid' }], + errors: [ + { + line: 4, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +class SafePromise extends Promise {} +let promise: SafePromise = Promise.resolve(5); +void promise.catch(); + `, + }, + ], + }, + ], }, { code: ` @@ -3554,7 +3595,22 @@ promise().finally(); options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, ], - errors: [{ line: 4, messageId: 'floatingVoid' }], + errors: [ + { + line: 4, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +class UnsafePromise extends Promise {} +let promise: () => UnsafePromise = async () => 5; +void promise().finally(); + `, + }, + ], + }, + ], }, { code: ` @@ -3565,7 +3621,22 @@ let promise: UnsafePromise = Promise.resolve(5); options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, ], - errors: [{ line: 4, messageId: 'floatingVoid' }], + errors: [ + { + line: 4, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +type UnsafePromise = Promise & { hey?: string }; +let promise: UnsafePromise = Promise.resolve(5); +void (0 ? promise.catch() : 2); + `, + }, + ], + }, + ], }, { code: ` @@ -3576,7 +3647,22 @@ null ?? promise().catch(); options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, ], - errors: [{ line: 4, messageId: 'floatingVoid' }], + errors: [ + { + line: 4, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +type UnsafePromise = Promise & { hey?: string }; +let promise: () => UnsafePromise = async () => 5; +void (null ?? promise().catch()); + `, + }, + ], + }, + ], }, { code: ` @@ -3617,7 +3703,22 @@ declare const myTag: (strings: TemplateStringsArray) => SafePromise; myTag\`abc\`; `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - errors: [{ line: 4, messageId: 'floatingVoid' }], + errors: [ + { + line: 4, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +type SafePromise = Promise & { __linterBrands?: string }; +declare const myTag: (strings: TemplateStringsArray) => SafePromise; +void myTag\`abc\`; + `, + }, + ], + }, + ], }, ], }); From 67b1a116b5311b2caf57e092d0fe326dbbdc3c9b Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Mon, 3 Jun 2024 10:11:02 -0400 Subject: [PATCH 11/17] Removed unused import --- packages/rule-tester/src/RuleTester.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 65e7bfc3d61a..e70f444475b9 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -41,7 +41,6 @@ import { freezeDeeply } from './utils/freezeDeeply'; import { getRuleOptionsSchema } from './utils/getRuleOptionsSchema'; import { hasOwnProperty } from './utils/hasOwnProperty'; import { getPlaceholderMatcher, interpolate } from './utils/interpolate'; -import { isReadonlyArray } from './utils/isReadonlyArray'; import { isSerializable } from './utils/serialization'; import * as SourceCodeFixer from './utils/SourceCodeFixer'; import { From 0c6fc1f8e149e06fb5022f2627c558a2d966af35 Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Tue, 25 Jun 2024 10:31:25 -0400 Subject: [PATCH 12/17] Fixed tests --- .../rules/prefer-nullish-coalescing.test.ts | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index e3547bb82591..c34419361462 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -1656,7 +1656,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 0 | 'foo' | undefined; +declare const x: null; x ?? y; `, }, @@ -1699,8 +1699,7 @@ undefined || y; { messageId: 'suggestNullish', output: ` -declare const x: 0 | 'foo' | undefined; -x ?? y; +undefined ?? y; `, }, ], @@ -1725,7 +1724,12 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: null; +enum Enum { + A = 0, + B = 1, + C = 2, +} +declare const x: Enum | undefined; x ?? y; `, }, @@ -1751,7 +1755,12 @@ x || y; { messageId: 'suggestNullish', output: ` -const x = undefined; +enum Enum { + A = 0, + B = 1, + C = 2, +} +declare const x: Enum.A | Enum.B | undefined; x ?? y; `, }, @@ -1777,7 +1786,13 @@ x || y; { messageId: 'suggestNullish', output: ` -null ?? y; +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare const x: Enum | undefined; +x ?? y; `, }, ], @@ -1802,7 +1817,13 @@ x || y; { messageId: 'suggestNullish', output: ` -undefined ?? y; +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare const x: Enum.A | Enum.B | undefined; +x ?? y; `, }, ], From f6c0a91138359dca9b8278e4bf2589c540a349a7 Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Tue, 25 Jun 2024 10:58:57 -0400 Subject: [PATCH 13/17] Fixed tests --- .../rules/prefer-nullish-coalescing.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index c34419361462..bd0cd13c4b5a 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -1673,6 +1673,15 @@ x || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +const x = undefined; +x ?? y; + `, + }, + ], }, ], }, @@ -1684,6 +1693,14 @@ null || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +null ?? y; + `, + }, + ], }, ], }, From 4eebf7df8d89a919fbc03b69c9405d118e9d066a Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Thu, 27 Jun 2024 13:23:00 +0000 Subject: [PATCH 14/17] Fixed tests order --- .../tests/eslint-base/eslint-base.test.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/rule-tester/tests/eslint-base/eslint-base.test.js b/packages/rule-tester/tests/eslint-base/eslint-base.test.js index 20d95917fc89..e87ece634534 100644 --- a/packages/rule-tester/tests/eslint-base/eslint-base.test.js +++ b/packages/rule-tester/tests/eslint-base/eslint-base.test.js @@ -2164,6 +2164,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/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( @@ -2206,21 +2221,6 @@ describe("RuleTester", () => { }, /A fatal parsing error occurred in suggestion fix\.\nError: .+\nSuggestion output:\n.+/u); }); - it("should fail when the suggestion property is neither a number nor an array", () => { - assert.throws(() => { - ruleTester.run("suggestions-basic", require("./fixtures/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 the suggestion description doesn't match", () => { assert.throws(() => { ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { From 0e08395b5fae44d1fa0d5e6b9ab3a7158aea2cfa Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Thu, 27 Jun 2024 15:06:21 +0000 Subject: [PATCH 15/17] Added back in forgotten condition --- packages/rule-tester/src/RuleTester.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index e70f444475b9..76546df6c256 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -885,6 +885,18 @@ export class RuleTester extends TestFramework { `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`, ); + const unsubstitutedPlaceholders = + getUnsubstitutedMessagePlaceholders( + message.message, + rule.meta.messages[message.messageId], + error.data, + ); + + assert.ok( + unsubstitutedPlaceholders.length === 0, + `The reported message has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(', ')}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? 'values' : 'value'} via the 'data' property in the context.report() call.`, + ); + if (hasOwnProperty(error, 'data')) { /* * if data was provided, then directly compare the returned message to a synthetic From cc5a70b93a695d046cfb48c7a279855c27a2eead Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Sat, 29 Jun 2024 10:52:53 -0400 Subject: [PATCH 16/17] Applied suggestions --- packages/rule-tester/src/RuleTester.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 76546df6c256..ae926072ab03 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -836,7 +836,7 @@ export class RuleTester extends TestFramework { // Just an error message. assertMessageMatches(message.message, error); assert.ok( - message.suggestions === void 0, + message.suggestions === undefined, `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) { @@ -972,9 +972,8 @@ export class RuleTester extends TestFramework { ? error.suggestions.length > 0 : Boolean(error.suggestions); const hasSuggestions = message.suggestions !== void 0; - // @ts-expect-error -- we purposely don't verify for undefined - const messageSuggestions: Linter.LintSuggestion[] = - message.suggestions; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const messageSuggestions = message.suggestions!; if (!hasSuggestions && expectsSuggestions) { assert.ok( From ea7f5f51bf9c7c3b267152dd699c2afa69eb049a Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Wed, 3 Jul 2024 11:17:56 -0400 Subject: [PATCH 17/17] Fixed tests --- .../tests/rules/no-empty-object-type.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts index 6494b8d6372f..ec418d10d249 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts @@ -546,6 +546,16 @@ let value: unknown; endColumn: 15, endLine: 1, messageId: 'noEmptyObject', + suggestions: [ + { + messageId: 'replaceEmptyObjectType', + output: 'type Base = object | null;', + }, + { + messageId: 'replaceEmptyObjectType', + output: 'type Base = unknown | null;', + }, + ], }, ], options: [{ allowWithName: 'Base' }], @@ -559,6 +569,16 @@ let value: unknown; endColumn: 15, endLine: 1, messageId: 'noEmptyObject', + suggestions: [ + { + messageId: 'replaceEmptyObjectType', + output: 'type Base = object;', + }, + { + messageId: 'replaceEmptyObjectType', + output: 'type Base = unknown;', + }, + ], }, ], options: [{ allowWithName: 'Mismatch' }],