diff --git a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.mdx b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.mdx index 94e647872a4f..0e185cb67997 100644 --- a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.mdx +++ b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.mdx @@ -168,38 +168,6 @@ You should be using `strictNullChecks` to ensure complete type-safety in your co If for some reason you cannot turn on `strictNullChecks`, but still want to use this rule - you can use this option to allow it - but know that the behavior of this rule is _undefined_ with the compiler option turned off. We will not accept bug reports if you are using this option. -## Fixes and Suggestions - -This rule provides following fixes and suggestions for particular types in boolean context: - -- `boolean` - Always allowed - no fix needed. -- `string` - (when `allowString` is `false`) - Provides following suggestions: - - Change condition to check string's length (`str` → `str.length > 0`) - - Change condition to check for empty string (`str` → `str !== ""`) - - Explicitly convert value to a boolean (`str` → `Boolean(str)`) -- `number` - (when `allowNumber` is `false`): - - For `array.length` - Provides **autofix**: - - Change condition to check for 0 (`array.length` → `array.length > 0`) - - For other number values - Provides following suggestions: - - Change condition to check for 0 (`num` → `num !== 0`) - - Change condition to check for NaN (`num` → `!Number.isNaN(num)`) - - Explicitly convert value to a boolean (`num` → `Boolean(num)`) -- `object | null | undefined` - (when `allowNullableObject` is `false`) - Provides **autofix**: - - Change condition to check for null/undefined (`maybeObj` → `maybeObj != null`) -- `boolean | null | undefined` - Provides following suggestions: - - Explicitly treat nullish value the same as false (`maybeBool` → `maybeBool ?? false`) - - Change condition to check for true/false (`maybeBool` → `maybeBool === true`) -- `string | null | undefined` - Provides following suggestions: - - Change condition to check for null/undefined (`maybeStr` → `maybeStr != null`) - - Explicitly treat nullish value the same as an empty string (`maybeStr` → `maybeStr ?? ""`) - - Explicitly convert value to a boolean (`maybeStr` → `Boolean(maybeStr)`) -- `number | null | undefined` - Provides following suggestions: - - Change condition to check for null/undefined (`maybeNum` → `maybeNum != null`) - - Explicitly treat nullish value the same as 0 (`maybeNum` → `maybeNum ?? 0`) - - Explicitly convert value to a boolean (`maybeNum` → `Boolean(maybeNum)`) -- `any` and `unknown` - Provides following suggestions: - - Explicitly convert value to a boolean (`value` → `Boolean(value)`) - ## When Not To Use It If your project isn't likely to experience bugs from falsy non-boolean values being used in logical conditions, you can skip enabling this rule. diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts index 395cf8524f8d..a42146a48fb9 100644 --- a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts +++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts @@ -43,6 +43,8 @@ export type MessageId = | 'conditionErrorOther' | 'conditionErrorString' | 'conditionFixCastBoolean' + | 'conditionFixCompareArrayLengthNonzero' + | 'conditionFixCompareArrayLengthZero' | 'conditionFixCompareEmptyString' | 'conditionFixCompareFalse' | 'conditionFixCompareNaN' @@ -63,7 +65,6 @@ export default createRule({ description: 'Disallow certain types in boolean expressions', requiresTypeChecking: true, }, - fixable: 'code', hasSuggestions: true, messages: { conditionErrorAny: @@ -102,6 +103,10 @@ export default createRule({ conditionFixCastBoolean: 'Explicitly convert value to a boolean (`Boolean(value)`)', + conditionFixCompareArrayLengthNonzero: + "Change condition to check array's length (`value.length > 0`)", + conditionFixCompareArrayLengthZero: + "Change condition to check array's length (`value.length === 0`)", conditionFixCompareEmptyString: 'Change condition to check for empty string (`value !== ""`)', conditionFixCompareFalse: @@ -571,23 +576,33 @@ export default createRule({ context.report({ node, messageId: 'conditionErrorNumber', - fix: getWrappingFixer({ - node: node.parent, - innerNode: node, - sourceCode: context.sourceCode, - wrap: code => `${code} === 0`, - }), + suggest: [ + { + messageId: 'conditionFixCompareArrayLengthZero', + fix: getWrappingFixer({ + node: node.parent, + innerNode: node, + sourceCode: context.sourceCode, + wrap: code => `${code} === 0`, + }), + }, + ], }); } else { // if (array.length) context.report({ node, messageId: 'conditionErrorNumber', - fix: getWrappingFixer({ - node, - sourceCode: context.sourceCode, - wrap: code => `${code} > 0`, - }), + suggest: [ + { + messageId: 'conditionFixCompareArrayLengthNonzero', + fix: getWrappingFixer({ + node, + sourceCode: context.sourceCode, + wrap: code => `${code} > 0`, + }), + }, + ], }); } } else if (isLogicalNegationExpression(node.parent)) { @@ -803,22 +818,32 @@ export default createRule({ context.report({ node, messageId: 'conditionErrorNullableEnum', - fix: getWrappingFixer({ - node: node.parent, - innerNode: node, - sourceCode: context.sourceCode, - wrap: code => `${code} == null`, - }), + suggest: [ + { + messageId: 'conditionFixCompareNullish', + fix: getWrappingFixer({ + node: node.parent, + innerNode: node, + sourceCode: context.sourceCode, + wrap: code => `${code} == null`, + }), + }, + ], }); } else { context.report({ node, messageId: 'conditionErrorNullableEnum', - fix: getWrappingFixer({ - node, - sourceCode: context.sourceCode, - wrap: code => `${code} != null`, - }), + suggest: [ + { + messageId: 'conditionFixCompareNullish', + fix: getWrappingFixer({ + node, + sourceCode: context.sourceCode, + wrap: code => `${code} != null`, + }), + }, + ], }); } } 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 d705d6402b2d..0c04929f4f0b 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -1557,24 +1557,74 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } }), // number (array.length) in boolean context - ...batchedSingleLineTests({ - code: noFormat` - if (![].length) {} - (a: number[]) => a.length && "..." - (...a: T) => a.length || "empty"; + + { + code: ` +if (![].length) { +} `, errors: [ - { column: 6, line: 2, messageId: 'conditionErrorNumber' }, - { column: 26, line: 3, messageId: 'conditionErrorNumber' }, - { column: 43, line: 4, messageId: 'conditionErrorNumber' }, + { + column: 6, + line: 2, + messageId: 'conditionErrorNumber', + suggestions: [ + { + messageId: 'conditionFixCompareArrayLengthZero', + output: ` +if ([].length === 0) { +} + `, + }, + ], + }, ], options: [{ allowNumber: false }], - output: ` - if ([].length === 0) {} - (a: number[]) => (a.length > 0) && "..." - (...a: T) => (a.length > 0) || "empty"; + }, + { + code: ` +(a: number[]) => a.length && '...'; `, - }), + errors: [ + { + column: 18, + line: 2, + messageId: 'conditionErrorNumber', + suggestions: [ + { + messageId: 'conditionFixCompareArrayLengthNonzero', + // not technically the same; changes from returning (nonzero) number to returning true + output: ` +(a: number[]) => (a.length > 0) && '...'; + `, + }, + ], + }, + ], + options: [{ allowNumber: false }], + }, + { + code: ` +(...a: T) => a.length || 'empty'; + `, + errors: [ + { + column: 35, + line: 2, + messageId: 'conditionErrorNumber', + suggestions: [ + { + messageId: 'conditionFixCompareArrayLengthNonzero', + // not technically the same; changes from returning (nonzero) number to returning true + output: ` +(...a: T) => (a.length > 0) || 'empty'; + `, + }, + ], + }, + ], + options: [{ allowNumber: false }], + }, // mixed `string | number` value in boolean context ...batchedSingleLineTests({ @@ -1899,10 +1949,10 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } endLine: 7, line: 7, messageId: 'conditionErrorNullableEnum', - }, - ], - options: [{ allowNullableEnum: false }], - output: ` + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` enum ExampleEnum { This = 0, That = 1, @@ -1911,6 +1961,11 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } if (theEnum != null) { } `, + }, + ], + }, + ], + options: [{ allowNullableEnum: false }], }, { code: ` @@ -1929,10 +1984,10 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } endLine: 7, line: 7, messageId: 'conditionErrorNullableEnum', - }, - ], - options: [{ allowNullableEnum: false }], - output: ` + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` enum ExampleEnum { This = 0, That = 1, @@ -1941,6 +1996,11 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } if (theEnum == null) { } `, + }, + ], + }, + ], + options: [{ allowNullableEnum: false }], }, { code: ` @@ -1959,10 +2019,10 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } endLine: 7, line: 7, messageId: 'conditionErrorNullableEnum', - }, - ], - options: [{ allowNullableEnum: false }], - output: ` + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` enum ExampleEnum { This, That, @@ -1971,6 +2031,11 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } if (theEnum == null) { } `, + }, + ], + }, + ], + options: [{ allowNullableEnum: false }], }, { code: ` @@ -1989,10 +2054,10 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } endLine: 7, line: 7, messageId: 'conditionErrorNullableEnum', - }, - ], - options: [{ allowNullableEnum: false }], - output: ` + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` enum ExampleEnum { This = '', That = 'a', @@ -2001,6 +2066,11 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } if (theEnum == null) { } `, + }, + ], + }, + ], + options: [{ allowNullableEnum: false }], }, { code: ` @@ -2019,10 +2089,10 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } endLine: 7, line: 7, messageId: 'conditionErrorNullableEnum', - }, - ], - options: [{ allowNullableEnum: false }], - output: ` + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` enum ExampleEnum { This = '', That = 0, @@ -2031,6 +2101,11 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } if (theEnum == null) { } `, + }, + ], + }, + ], + options: [{ allowNullableEnum: false }], }, { code: ` @@ -2049,10 +2124,10 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } endLine: 7, line: 7, messageId: 'conditionErrorNullableEnum', - }, - ], - options: [{ allowNullableEnum: false }], - output: ` + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` enum ExampleEnum { This = 'one', That = 'two', @@ -2061,6 +2136,11 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } if (theEnum == null) { } `, + }, + ], + }, + ], + options: [{ allowNullableEnum: false }], }, { code: ` @@ -2079,10 +2159,10 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } endLine: 7, line: 7, messageId: 'conditionErrorNullableEnum', - }, - ], - options: [{ allowNullableEnum: false }], - output: ` + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` enum ExampleEnum { This = 1, That = 2, @@ -2091,6 +2171,11 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } if (theEnum == null) { } `, + }, + ], + }, + ], + options: [{ allowNullableEnum: false }], }, // nullable mixed enum in boolean context @@ -2110,16 +2195,21 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } endLine: 6, line: 6, messageId: 'conditionErrorNullableEnum', - }, - ], - options: [{ allowNullableEnum: false }], - output: ` + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` enum ExampleEnum { This = 0, That = 'one', } (value?: ExampleEnum) => ((value != null) ? 1 : 0); `, + }, + ], + }, + ], + options: [{ allowNullableEnum: false }], }, { // falsy string and truthy number @@ -2137,16 +2227,21 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } endLine: 6, line: 6, messageId: 'conditionErrorNullableEnum', - }, - ], - options: [{ allowNullableEnum: false }], - output: ` + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` enum ExampleEnum { This = '', That = 1, } (value?: ExampleEnum) => ((value == null) ? 1 : 0); `, + }, + ], + }, + ], + options: [{ allowNullableEnum: false }], }, { // truthy string and truthy number @@ -2164,16 +2259,21 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } endLine: 6, line: 6, messageId: 'conditionErrorNullableEnum', - }, - ], - options: [{ allowNullableEnum: false }], - output: ` + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` enum ExampleEnum { This = 'this', That = 1, } (value?: ExampleEnum) => ((value == null) ? 1 : 0); `, + }, + ], + }, + ], + options: [{ allowNullableEnum: false }], }, { // falsy string and falsy number @@ -2191,16 +2291,21 @@ if (((Boolean('')) && {}) || (foo && void 0)) { } endLine: 6, line: 6, messageId: 'conditionErrorNullableEnum', - }, - ], - options: [{ allowNullableEnum: false }], - output: ` + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` enum ExampleEnum { This = '', That = 0, } (value?: ExampleEnum) => ((value == null) ? 1 : 0); `, + }, + ], + }, + ], + options: [{ allowNullableEnum: false }], }, // any in boolean context