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' }], pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy