From d4747cd8cc9dad2bf2cb64e1c0e8980ce34d82c7 Mon Sep 17 00:00:00 2001 From: Kazuya Iijima <50566849+kazizi55@users.noreply.github.com> Date: Sun, 5 Feb 2023 11:15:04 +0900 Subject: [PATCH] feat(eslint-plugin): [strict-boolean-expressions] add allow nullable enum to strict boolean expressions (#6096) * feat: add allowNullableEnum option * test: add allowNullableEnum option * fix: lint error * chore: erase indent diff * fix: error column number * test: fix output * test: fix output format * test: add !nullableEnum pattern * Update packages/eslint-plugin/src/rules/strict-boolean-expressions.ts Co-authored-by: Josh Goldberg * Update packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts Co-authored-by: Josh Goldberg * chore: yarn lint --fix * perf: use some instead of length * test: fix output * test: split into two cases * docs: add allowNullableEnum * Update packages/eslint-plugin/src/rules/strict-boolean-expressions.ts Co-authored-by: Josh Goldberg * perf: use some instead of filter * Update packages/eslint-plugin/docs/rules/strict-boolean-expressions.md Co-authored-by: Josh Goldberg * Update packages/eslint-plugin/docs/rules/strict-boolean-expressions.md Co-authored-by: Josh Goldberg * Update packages/eslint-plugin/src/rules/strict-boolean-expressions.ts Co-authored-by: Josh Goldberg * test: add case * fix: add string condition * Fix lil lint issue * Test fix --------- Co-authored-by: Josh Goldberg --- .../docs/rules/strict-boolean-expressions.md | 6 + .../src/rules/strict-boolean-expressions.ts | 43 ++++ .../rules/strict-boolean-expressions.test.ts | 184 ++++++++++++++++++ 3 files changed, 233 insertions(+) diff --git a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md index b7e8c13a9be..45a7f8f9496 100644 --- a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md +++ b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md @@ -124,6 +124,12 @@ Allows `number | null | undefined` in a boolean context. This is unsafe because nullable numbers can be either a falsy number or nullish. Set this to `true` if you don't mind implicitly treating zero or NaN the same as a nullish value. +### `allowNullableEnum` + +Allows `enum | null | undefined` in a boolean context. +This is unsafe because nullable enums can be either a falsy number or nullish. +Set this to `true` if you don't mind implicitly treating an enum whose value is zero the same as a nullish value. + ### `allowAny` Allows `any` in a boolean context. diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts index 1e327a8a4b8..bf58727df29 100644 --- a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts +++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts @@ -13,6 +13,7 @@ export type Options = [ allowNullableBoolean?: boolean; allowNullableString?: boolean; allowNullableNumber?: boolean; + allowNullableEnum?: boolean; allowAny?: boolean; allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; }, @@ -29,6 +30,7 @@ export type MessageId = | 'conditionErrorNullableNumber' | 'conditionErrorObject' | 'conditionErrorNullableObject' + | 'conditionErrorNullableEnum' | 'noStrictNullCheck' | 'conditionFixDefaultFalse' | 'conditionFixDefaultEmptyString' @@ -63,6 +65,7 @@ export default util.createRule({ allowNullableBoolean: { type: 'boolean' }, allowNullableString: { type: 'boolean' }, allowNullableNumber: { type: 'boolean' }, + allowNullableEnum: { type: 'boolean' }, allowAny: { type: 'boolean' }, allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { type: 'boolean', @@ -102,6 +105,9 @@ export default util.createRule({ conditionErrorNullableObject: 'Unexpected nullable object value in conditional. ' + 'An explicit null check is required.', + conditionErrorNullableEnum: + 'Unexpected nullable enum value in conditional. ' + + 'Please handle the nullish/zero/NaN cases explicitly.', noStrictNullCheck: 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', @@ -137,6 +143,7 @@ export default util.createRule({ allowNullableBoolean: false, allowNullableString: false, allowNullableNumber: false, + allowNullableEnum: true, allowAny: false, allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, }, @@ -718,6 +725,35 @@ export default util.createRule({ return; } + // nullable enum + if (is('nullish', 'number', 'enum') || is('nullish', 'string', 'enum')) { + if (!options.allowNullableEnum) { + if (isLogicalNegationExpression(node.parent!)) { + context.report({ + node, + messageId: 'conditionErrorNullableEnum', + fix: util.getWrappingFixer({ + sourceCode, + node: node.parent, + innerNode: node, + wrap: code => `${code} == null`, + }), + }); + } else { + context.report({ + node, + messageId: 'conditionErrorNullableEnum', + fix: util.getWrappingFixer({ + sourceCode, + node, + wrap: code => `${code} != null`, + }), + }); + } + } + return; + } + // any if (is('any')) { if (!options.allowAny) { @@ -753,6 +789,7 @@ export default util.createRule({ | 'number' | 'truthy number' | 'object' + | 'enum' | 'any' | 'never'; @@ -814,6 +851,12 @@ export default util.createRule({ } } + if ( + types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.EnumLike)) + ) { + variantTypes.add('enum'); + } + if ( types.some( type => 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 9261acda41b..f3b6deef1bf 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -133,6 +133,39 @@ ruleTester.run('strict-boolean-expressions', rule, { `, }), + // nullable enum in boolean context + { + code: ` + enum ExampleEnum { + This = 0, + That = 1, + } + const rand = Math.random(); + let theEnum: ExampleEnum | null = null; + if (rand < 0.3) { + theEnum = ExampleEnum.This; + } + if (theEnum) { + } + `, + options: [{ allowNullableEnum: true }], + }, + { + code: ` + enum ExampleEnum { + This = 0, + That = 1, + } + const rand = Math.random(); + let theEnum: ExampleEnum | null = null; + if (rand < 0.3) { + theEnum = ExampleEnum.This; + } + if (!theEnum) { + } + `, + options: [{ allowNullableEnum: true }], + }, { code: ` declare const x: string[] | null; @@ -965,6 +998,157 @@ if (y) { ], }), + // nullable enum in boolean context + { + options: [{ allowNullableEnum: false }], + code: ` + enum ExampleEnum { + This = 0, + That = 1, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (theEnum) { + } + `, + errors: [ + { + line: 7, + column: 13, + messageId: 'conditionErrorNullableEnum', + endLine: 7, + endColumn: 20, + }, + ], + output: ` + enum ExampleEnum { + This = 0, + That = 1, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (theEnum != null) { + } + `, + }, + { + options: [{ allowNullableEnum: false }], + code: ` + enum ExampleEnum { + This = 0, + That = 1, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (!theEnum) { + } + `, + errors: [ + { + line: 7, + column: 14, + messageId: 'conditionErrorNullableEnum', + endLine: 7, + endColumn: 21, + }, + ], + output: ` + enum ExampleEnum { + This = 0, + That = 1, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (theEnum == null) { + } + `, + }, + { + options: [{ allowNullableEnum: false }], + code: ` + enum ExampleEnum { + This, + That, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (!theEnum) { + } + `, + errors: [ + { + line: 7, + column: 14, + messageId: 'conditionErrorNullableEnum', + endLine: 7, + endColumn: 21, + }, + ], + output: ` + enum ExampleEnum { + This, + That, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (theEnum == null) { + } + `, + }, + { + options: [{ allowNullableEnum: false }], + code: ` + enum ExampleEnum { + This = '', + That = 'a', + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (!theEnum) { + } + `, + errors: [ + { + line: 7, + column: 14, + messageId: 'conditionErrorNullableEnum', + endLine: 7, + endColumn: 21, + }, + ], + output: ` + enum ExampleEnum { + This = '', + That = 'a', + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (theEnum == null) { + } + `, + }, + { + options: [{ allowNullableEnum: false }], + code: ` + enum ExampleEnum { + This = '', + That = 0, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (!theEnum) { + } + `, + errors: [ + { + line: 7, + column: 14, + messageId: 'conditionErrorNullableEnum', + endLine: 7, + endColumn: 21, + }, + ], + output: ` + enum ExampleEnum { + This = '', + That = 0, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (theEnum == null) { + } + `, + }, // any in boolean context ...batchedSingleLineTests({ code: noFormat`