From cd1448240dca11762fcb9c10e18bb6541a840485 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 10 May 2020 21:11:01 +0200 Subject: [PATCH] feat(eslint-plugin): [strict-boolean-expression] rework options (#1631) --- .../docs/rules/strict-boolean-expressions.md | 123 +- .../src/rules/strict-boolean-expressions.ts | 189 ++- .../rules/strict-boolean-expressions.test.ts | 1398 +++-------------- 3 files changed, 421 insertions(+), 1289 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md index cec621588ef..07284154a31 100644 --- a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md +++ b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md @@ -1,67 +1,122 @@ # Restricts the types allowed in boolean expressions (`strict-boolean-expressions`) -Requires that any boolean expression is limited to true booleans rather than -casting another primitive to a boolean at runtime. +Forbids usage of non-boolean types in expressions where a boolean is expected. +`boolean` and `never` types are always allowed. +Additional types which are considered safe in a boolean context can be configured via options. -It is useful to be explicit, for example, if you were trying to check if a -number was defined. Doing `if (number)` would evaluate to `false` if `number` -was defined and `0`. This rule forces these expressions to be explicit and to -strictly use booleans. +The following nodes are considered boolean expressions and their type is checked: -The following nodes are checked: - -- Arguments to the `!`, `&&`, and `||` operators -- The condition in a conditional expression `(cond ? x : y)` +- Argument to the logical negation operator (`!arg`). +- The condition in a conditional expression (`cond ? x : y`). - Conditions for `if`, `for`, `while`, and `do-while` statements. +- Operands of logical binary operators (`lhs || rhs` and `lhs && rhs`). + - Right-hand side operand is ignored when it's not a descendant of another boolean expression. + This is to allow usage of boolean operators for their short-circuiting behavior. + +## Examples Examples of **incorrect** code for this rule: ```ts -const number = 0; -if (number) { - return; +// nullable numbers are considered unsafe by default +let num: number | undefined = 0; +if (num) { + console.log('num is defined'); +} + +// nullable strings are considered unsafe by default +let str: string | null = null; +if (!str) { + console.log('str is empty'); } -let foo = bar || 'foobar'; +// nullable booleans are considered unsafe by default +function foo(bool?: boolean) { + if (bool) { + bar(); + } +} -let undefinedItem; -let foo = undefinedItem ? 'foo' : 'bar'; +// `any`, unconstrained generics and unions of more than one primitive type are disallowed +const foo = (arg: T) => (arg ? 1 : 0); -let str = 'foo'; -while (str) { - break; +// always-truthy and always-falsy types are disallowed +let obj = {}; +while (obj) { + obj = getObj(); } ``` Examples of **correct** code for this rule: -```ts -const number = 0; -if (typeof number !== 'undefined') { - return; +```tsx +// Using logical operators for their side effects is allowed +const Component = () => { + const entry = map.get('foo') || {}; + return entry &&

Name: {entry.name}

; +}; + +// nullable values should be checked explicitly against null or undefined +let num: number | undefined = 0; +if (num != null) { + console.log('num is defined'); } -let foo = typeof bar !== 'undefined' ? bar : 'foobar'; - -let undefinedItem; -let foo = typeof undefinedItem !== 'undefined' ? 'foo' : 'bar'; +let str: string | null = null; +if (str != null && !str) { + console.log('str is empty'); +} -let str = 'foo'; -while (typeof str !== 'undefined') { - break; +function foo(bool?: boolean) { + if (bool ?? false) { + bar(); + } } + +// `any` types should be cast to boolean explicitly +const foo = (arg: any) => (Boolean(arg) ? 1 : 0); ``` ## Options Options may be provided as an object with: -- `allowNullable` to allow `undefined` and `null` in addition to `boolean` as a type of all boolean expressions. (`false` by default). -- `allowSafe` to allow non-falsy types (i.e. non string / number / boolean) in addition to `boolean` as a type of all boolean expressions. (`false` by default). -- `ignoreRhs` to skip the check on the right hand side of expressions like `a && b` or `a || b` - allows these operators to be used for their short-circuiting behavior. (`false` by default). +- `allowString` (`true` by default) - + Allows `string` in a boolean context. + This is safe because strings have only one falsy value (`""`). + Set this to `false` if you prefer the explicit `str != ""` or `str.length > 0` style. + +- `allowNumber` (`true` by default) - + Allows `number` in a boolean context. + This is safe because numbers have only two falsy values (`0` and `NaN`). + Set this to `false` if you prefer the explicit `num != 0` and `!Number.isNaN(num)` style. + +- `allowNullableObject` (`true` by default) - + Allows `object | function | symbol | null | undefined` in a boolean context. + This is safe because objects, functions and symbols don't have falsy values. + Set this to `false` if you prefer the explicit `obj != null` style. + +- `allowNullableBoolean` (`false` by default) - + Allows `boolean | null | undefined` in a boolean context. + This is unsafe because nullable booleans can be either `false` or nullish. + Set this to `false` if you want to enforce explicit `bool ?? false` or `bool ?? true` style. + Set this to `true` if you don't mind implicitly treating false the same as a nullish value. + +- `allowNullableString` (`false` by default) - + Allows `string | null | undefined` in a boolean context. + This is unsafe because nullable strings can be either an empty string or nullish. + Set this to `true` if you don't mind implicitly treating an empty string the same as a nullish value. + +- `allowNullableNumber` (`false` by default) - + 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. + +- `allowAny` (`false` by default) - + Allows `any` in a boolean context. ## Related To - TSLint: [strict-boolean-expressions](https://palantir.github.io/tslint/rules/strict-boolean-expressions) -- [no-unnecessary-condition](./no-unnecessary-condition.md) - essentially a less opinionated alternative to this rule. `strict-boolean-expressions` enforces a specific code style, while `no-unnecessary-condition` is about correctness. +- [no-unnecessary-condition](./no-unnecessary-condition.md) - Similar rule which reports always-truthy and always-falsy values in conditions diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts index 265bc05a41f..df2b59e12ec 100644 --- a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts +++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts @@ -6,15 +6,19 @@ import * as ts from 'typescript'; import * as tsutils from 'tsutils'; import * as util from '../util'; -type Options = [ +export type Options = [ { - ignoreRhs?: boolean; - allowNullable?: boolean; - allowSafe?: boolean; + allowString?: boolean; + allowNumber?: boolean; + allowNullableObject?: boolean; + allowNullableBoolean?: boolean; + allowNullableString?: boolean; + allowNullableNumber?: boolean; + allowAny?: boolean; }, ]; -type MessageId = +export type MessageId = | 'conditionErrorOther' | 'conditionErrorAny' | 'conditionErrorNullish' @@ -40,15 +44,13 @@ export default util.createRule({ { type: 'object', properties: { - ignoreRhs: { - type: 'boolean', - }, - allowNullable: { - type: 'boolean', - }, - allowSafe: { - type: 'boolean', - }, + allowString: { type: 'boolean' }, + allowNumber: { type: 'boolean' }, + allowNullableObject: { type: 'boolean' }, + allowNullableBoolean: { type: 'boolean' }, + allowNullableString: { type: 'boolean' }, + allowNullableNumber: { type: 'boolean' }, + allowAny: { type: 'boolean' }, }, additionalProperties: false, }, @@ -88,9 +90,9 @@ export default util.createRule({ }, defaultOptions: [ { - ignoreRhs: false, - allowNullable: false, - allowSafe: false, + allowString: true, + allowNumber: true, + allowNullableObject: true, }, ], create(context, [options]) { @@ -109,14 +111,14 @@ export default util.createRule({ 'UnaryExpression[operator="!"]': checkUnaryLogicalExpression, }; - type ExpressionWithTest = + type TestExpression = | TSESTree.ConditionalExpression | TSESTree.DoWhileStatement | TSESTree.ForStatement | TSESTree.IfStatement | TSESTree.WhileStatement; - function checkTestExpression(node: ExpressionWithTest): void { + function checkTestExpression(node: TestExpression): void { if (node.test == null) { return; } @@ -128,52 +130,35 @@ export default util.createRule({ } /** - * This function analyzes the type of a boolean expression node and checks if it is allowed. - * It can recurse when checking nested logical operators, so that only the outermost expressions are reported. + * This function analyzes the type of a node and checks if it is allowed in a boolean context. + * It can recurse when checking nested logical operators, so that only the outermost operands are reported. + * The right operand of a logical expression is ignored unless it's a part of a test expression (if/while/ternary/etc). * @param node The AST node to check. - * @param isRoot Whether it is the root of a logical expression and there was no recursion yet. - * @returns `true` if there was an error reported. + * @param isTestExpr Whether the node is a descendant of a test expression. */ - function checkNode(node: TSESTree.Node, isRoot = false): boolean { + function checkNode(node: TSESTree.Node, isTestExpr = false): void { // prevent checking the same node multiple times if (checkedNodes.has(node)) { - return false; + return; } checkedNodes.add(node); - // for logical operator, we also check its operands + // for logical operator, we check its operands if ( node.type === AST_NODE_TYPES.LogicalExpression && node.operator !== '??' ) { - let hasError = false; - if (checkNode(node.left)) { - hasError = true; - } - if (!options.ignoreRhs) { - if (checkNode(node.right)) { - hasError = true; - } - } - // if this logical operator is not the root of a logical expression - // we only check its operands and return - if (!isRoot) { - return hasError; - } - // if this is the root of a logical expression - // we want to check its resulting type too - else { - // ...unless there already was an error, we exit so we don't double-report - if (hasError) { - return true; - } + checkNode(node.left, isTestExpr); + + // we ignore the right operand when not in a context of a test expression + if (isTestExpr) { + checkNode(node.right, isTestExpr); } + return; } const tsNode = service.esTreeNodeToTSNodeMap.get(node); const type = util.getConstrainedTypeAtLocation(checker, tsNode); - let messageId: MessageId | undefined; - const types = inspectVariantTypes(tsutils.unionTypeParts(type)); const is = (...wantedTypes: readonly VariantType[]): boolean => @@ -183,79 +168,87 @@ export default util.createRule({ // boolean if (is('boolean')) { // boolean is always okay - return false; + return; } + // never if (is('never')) { // never is always okay - return false; + return; } + // nullish - else if (is('nullish')) { + if (is('nullish')) { // condition is always false - messageId = 'conditionErrorNullish'; + context.report({ node, messageId: 'conditionErrorNullish' }); + return; } + // nullable boolean - else if (is('nullish', 'boolean')) { - if (!options.allowNullable) { - messageId = 'conditionErrorNullableBoolean'; + if (is('nullish', 'boolean')) { + if (!options.allowNullableBoolean) { + context.report({ node, messageId: 'conditionErrorNullableBoolean' }); } + return; } + // string - else if (is('string')) { - messageId = 'conditionErrorString'; + if (is('string')) { + if (!options.allowString) { + context.report({ node, messageId: 'conditionErrorString' }); + } + return; } + // nullable string - else if (is('nullish', 'string')) { - messageId = 'conditionErrorNullableString'; + if (is('nullish', 'string')) { + if (!options.allowNullableString) { + context.report({ node, messageId: 'conditionErrorNullableString' }); + } + return; } + // number - else if (is('number')) { - messageId = 'conditionErrorNumber'; + if (is('number')) { + if (!options.allowNumber) { + context.report({ node, messageId: 'conditionErrorNumber' }); + } + return; } + // nullable number - else if (is('nullish', 'number')) { - messageId = 'conditionErrorNullableNumber'; + if (is('nullish', 'number')) { + if (!options.allowNullableNumber) { + context.report({ node, messageId: 'conditionErrorNullableNumber' }); + } + return; } + // object - else if (is('object')) { + if (is('object')) { // condition is always true - if (!options.allowSafe) { - messageId = 'conditionErrorObject'; - } + context.report({ node, messageId: 'conditionErrorObject' }); + return; } + // nullable object - else if (is('nullish', 'object')) { - if (!options.allowSafe || !options.allowNullable) { - messageId = 'conditionErrorNullableObject'; - } - } - // boolean/object - else if (is('boolean', 'object')) { - if (!options.allowSafe) { - messageId = 'conditionErrorOther'; - } - } - // nullable boolean/object - else if (is('nullish', 'boolean', 'object')) { - if (!options.allowSafe || !options.allowNullable) { - messageId = 'conditionErrorOther'; + if (is('nullish', 'object')) { + if (!options.allowNullableObject) { + context.report({ node, messageId: 'conditionErrorNullableObject' }); } + return; } + // any - else if (is('any')) { - messageId = 'conditionErrorAny'; - } - // other - else { - messageId = 'conditionErrorOther'; + if (is('any')) { + if (!options.allowAny) { + context.report({ node, messageId: 'conditionErrorAny' }); + } + return; } - if (messageId != null) { - context.report({ node, messageId }); - return true; - } - return false; + // other + context.report({ node, messageId: 'conditionErrorOther' }); } /** The types we care about */ @@ -300,7 +293,12 @@ export default util.createRule({ } if ( - types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.NumberLike)) + types.some(type => + tsutils.isTypeFlagSet( + type, + ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike, + ), + ) ) { variantTypes.add('number'); } @@ -316,6 +314,7 @@ export default util.createRule({ ts.TypeFlags.BooleanLike | ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | + ts.TypeFlags.BigIntLike | ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never, 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 daa619b84ee..f72739fb004 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -1,1239 +1,317 @@ -import rule from '../../src/rules/strict-boolean-expressions'; +import rule, { + Options, + MessageId, +} from '../../src/rules/strict-boolean-expressions'; import { - RuleTester, - getFixturesRootDir, batchedSingleLineTests, + getFixturesRootDir, + RuleTester, noFormat, } from '../RuleTester'; -const rootPath = getFixturesRootDir(); - const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', parserOptions: { - tsconfigRootDir: rootPath, + tsconfigRootDir: getFixturesRootDir(), project: './tsconfig.json', }, }); ruleTester.run('strict-boolean-expressions', rule, { valid: [ - ` - let val = true; - let bool = !val; - let bool2 = true || val; - let bool3 = true && val; - `, - ` - let a = 0; - let u1 = typeof a; - let u2 = -a; - let u3 = ~a; - `, - ` - const bool1 = true; - const bool2 = false; - if (true) { - return; - } - - if (bool1) { - return; - } - - if (bool1 && bool2) { - return; - } + // boolean in boolean context + ...batchedSingleLineTests({ + code: noFormat` + true ? "a" : "b"; + if (false) {} + while (true) {} + for (; false;) {} + !true; + false || 123; + true && "foo"; + !(false || true); + true && false ? true : false; + false && true || false; + false && true || []; + (false && 1) || (true && 2); + declare const x: boolean; if (x) {} + (x: boolean) => !x; + (x: T) => x ? 1 : 0; + declare const x: never; if (x) {} + `, + }), - if (bool1 || bool2) { - return; - } + // string in boolean context + ...batchedSingleLineTests({ + code: noFormat` + if ("") {} + while ("x") {} + for (; "";) {} + "" && "1" || x; + declare const x: string; if (x) {} + (x: string) => !x; + (x: T) => x ? 1 : 0; + `, + }), - if ((bool1 && bool2) || bool1 || bool2) { - return; - } - `, - ` - const bool1 = true; - const bool2 = false; - const res1 = true ? true : false; - const res2 = bool1 && bool2 ? true : false; - const res3 = bool1 || bool2 ? true : false; - const res4 = (bool1 && bool2) || bool1 || bool2 ? true : false; - `, - ` - for (let i = 0; true; i++) { - break; - } - `, - ` - const bool = true; - for (let i = 0; bool; i++) { - break; - } - `, - ` - const bool1 = true; - const bool2 = false; - for (let i = 0; bool1 && bool2; i++) { - break; - } - `, - ` - const bool1 = true; - const bool2 = false; - for (let i = 0; bool1 || bool2; i++) { - break; - } - `, - ` - const bool1 = true; - const bool2 = false; - for (let i = 0; (bool1 && bool2) || bool1 || bool2; i++) { - break; - } - `, - ` - while (true) { - break; - } - `, - ` - const bool = true; - while (bool) { - break; - } - `, - ` - const bool1 = true; - const bool2 = false; - while (bool1 && bool2) { - break; - } - `, - ` - const bool1 = true; - const bool2 = false; - while (bool1 || bool2) { - break; - } - `, - ` - const bool1 = true; - const bool2 = false; - while ((bool1 && bool2) || bool1 || bool2) { - break; - } - `, - ` - do { - break; - } while (true); - `, - ` - const bool = true; - do { - break; - } while (bool); - `, - ` - const bool1 = true; - const bool2 = false; - do { - break; - } while (bool1 && bool2); - `, - ` - const bool1 = true; - const bool2 = false; - do { - break; - } while (bool1 || bool2); - `, - ` - const bool1 = true; - const bool2 = false; - do { - break; - } while ((bool1 && bool2) || bool1 || bool2); - `, - ` - function foo(arg: T) { - return !arg; - } - `, - { - options: [{ ignoreRhs: true }], - code: ` - const obj = { x: 1 }; - const bool = false; - const boolOrObj = bool || obj; - const boolAndObj = bool && obj; + // number in boolean context + ...batchedSingleLineTests({ + code: noFormat` + if (0) {} + while (1n) {} + for (; Infinity;) {} + 0 / 0 && 1 + 2 || x; + declare const x: number; if (x) {} + (x: bigint) => !x; + (x: T) => x ? 1 : 0; `, - }, - ...batchedSingleLineTests({ - options: [{ allowNullable: true }], - code: ` - const f1 = (x?: boolean) => (x ? 1 : 0); - const f2 = (x: boolean | null) => (x ? 1 : 0); - const f3 = (x?: true | null) => (x ? 1 : 0); - const f4 = (x?: false) => (x ? 1 : 0); + }), + + // nullable object in boolean context + ...batchedSingleLineTests({ + code: noFormat` + declare const x: null | object; if (x) {} + (x?: { a: any }) => !x; + (x: T) => x ? 1 : 0; `, }), - ` - declare const x: string | null; - y = x ?? 'foo'; - `, - ...batchedSingleLineTests({ - options: [{ allowSafe: true }], - code: ` - const f1 = (x: boolean | { a: string }) => (x ? 1 : 0); - const f2 = (x: true | { a: string }) => (x ? 1 : 0); - const f3 = (x: { a: string } | false) => (x ? 1 : 0); + + // nullable boolean in boolean context + ...batchedSingleLineTests({ + options: [{ allowNullableBoolean: true }], + code: noFormat` + declare const x: boolean | null; if (x) {} + (x?: boolean) => !x; + (x: T) => x ? 1 : 0; `, }), - ...batchedSingleLineTests({ - options: [{ allowNullable: true, allowSafe: true }], - code: ` - const f1 = (x?: boolean | { a?: 1 }) => (x ? 1 : 0); - const f2 = (x: { a?: 1 } | { b?: 'a' } | null) => (x ? 1 : 0); - const f3 = (x?: { a?: 1 } | { b?: 'a' } | null) => (x ? 1 : 0); - const f4 = (x?: { b?: 'a' } | true) => (x ? 1 : 0); - const f5 = (g?: (x: number) => number) => (g ? g(1) : 0); + + // nullable string in boolean context + ...batchedSingleLineTests({ + options: [{ allowNullableString: true }], + code: noFormat` + declare const x: string | null; if (x) {} + (x?: string) => !x; + (x: T) => x ? 1 : 0; `, }), - ...batchedSingleLineTests({ - options: [{ allowNullable: true, allowSafe: true, ignoreRhs: true }], - code: ` - const f1 = (x?: { a: null }) => x && x.foo && x.foo.bar; - const f2 = (g?: (x: number) => number) => g && g(1); + + // nullable number in boolean context + ...batchedSingleLineTests({ + options: [{ allowNullableNumber: true }], + code: noFormat` + declare const x: number | null; if (x) {} + (x?: number) => !x; + (x: T) => x ? 1 : 0; `, }), - ` - declare let x: never; - if (x) { - } - `, - ...batchedSingleLineTests({ + + // any in boolean context + ...batchedSingleLineTests({ + options: [{ allowAny: true }], code: noFormat` - function f1(x: never) { return !x; } - function f2(x: never) { return x ? 1 : 0; } - function f3(x: never, y: never) { return x && y; } - function f5(x: never | boolean) { if (!x) { } } + declare const x: any; if (x) {} + (x) => !x; + (x: T) => x ? 1 : 0; `, }), ], invalid: [ - { - code: ` - let val = 'foo'; - let bool = !val; - `, - errors: [ - { - messageId: 'conditionErrorString', - line: 3, - column: 21, - }, - ], - }, - { - code: ` - let val; - let bool = !val; - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 3, - column: 21, - }, - ], - }, - { - code: ` - let val = 1; - let bool = true && val; - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 3, - column: 28, - }, - ], - }, - { - code: ` - let val; - let bool = true && val; - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 3, - column: 28, - }, - ], - }, - { - code: ` - let val = 1; - let bool = true || val; - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 3, - column: 28, - }, - ], - }, - { - code: ` - let val; - let bool = true || val; - `, - errors: [ - { - messageId: 'conditionErrorAny', - line: 3, - column: 28, - }, - ], - }, - { - code: ` - let num = 1; - let str = 'foo'; - let val = null; - let bool = true && (val || num || str); - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 5, - column: 29, - }, - { - messageId: 'conditionErrorNumber', - line: 5, - column: 36, - }, - { - messageId: 'conditionErrorString', - line: 5, - column: 43, - }, - ], - }, - { - code: ` - if (1) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 2, - column: 13, - }, - ], - }, - { - code: ` - if (undefined) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 2, - column: 13, - }, - ], - }, - { - code: ` - let item = 'foo'; - if (item) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorString', - line: 3, - column: 13, - }, - ], - }, - { - code: ` - let item; - if (item) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 3, - column: 13, - }, - ], - }, - { - code: ` - let item1 = true; - let item2 = 1; - if (item1 && item2) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 4, - column: 22, - }, - ], - }, - { - code: ` - let item1 = 1; - let item2 = true; - if (item1 && item2) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 4, - column: 13, - }, - ], - }, - { - code: ` - let item1; - let item2 = true; - if (item1 && item2) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 4, - column: 13, - }, - ], - }, - { - code: ` - let item1 = true; - let item2 = 1; - if (item1 || item2) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 4, - column: 22, - }, - ], - }, - { - code: ` - let item1 = 1; - let item2 = true; - if (item1 || item2) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 4, - column: 13, - }, - ], - }, - { - code: ` - let item1; - let item2 = true; - if (item1 || item2) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 4, - column: 13, - }, - ], - }, - { - code: ` - const bool = 'foo' ? true : false; - `, - errors: [ - { - messageId: 'conditionErrorString', - line: 2, - column: 22, - }, - ], - }, - { - code: ` - const bool = undefined ? true : false; - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 2, - column: 22, - }, - ], - }, - { - code: ` - let item = 1; - const bool = item ? true : false; - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 3, - column: 22, - }, - ], - }, - { - code: ` - let item; - const bool = item ? true : false; - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 3, - column: 22, - }, - ], - }, - { - code: ` - let item1 = 1; - let item2 = false; - const bool = item1 && item2 ? true : false; - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 4, - column: 22, - }, - ], - }, - { - code: ` - let item1 = true; - let item2 = 1; - const bool = item1 && item2 ? true : false; - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 4, - column: 31, - }, - ], - }, - { - code: ` - let item1 = true; - let item2; - const bool = item1 && item2 ? true : false; - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 4, - column: 31, - }, - ], - }, - { - code: ` - let item1 = 1; - let item2 = false; - const bool = item1 || item2 ? true : false; - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 4, - column: 22, - }, - ], - }, - { - code: ` - let item1 = true; - let item2 = 1; - const bool = item1 || item2 ? true : false; - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 4, - column: 31, - }, - ], - }, - { - code: ` - let item1 = true; - let item2; - const bool = item1 || item2 ? true : false; - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 4, - column: 31, - }, - ], - }, - { - code: ` - for (let i = 0; 1; i++) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 2, - column: 25, - }, + // non-boolean in RHS of test expression + ...batchedSingleLineTests({ + options: [ + { allowString: false, allowNumber: false, allowNullableObject: false }, ], - }, - { - code: ` - for (let i = 0; undefined; i++) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 2, - column: 25, - }, - ], - }, - { - code: ` - let bool = 1; - for (let i = 0; bool; i++) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 3, - column: 25, - }, - ], - }, - { - code: ` - let bool; - for (let i = 0; bool; i++) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 3, - column: 25, - }, - ], - }, - { - code: ` - let bool1 = 'foo'; - let bool2 = true; - for (let i = 0; bool1 && bool2; i++) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorString', - line: 4, - column: 25, - }, - ], - }, - { - code: ` - let bool1; - let bool2 = true; - for (let i = 0; bool1 && bool2; i++) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 4, - column: 25, - }, - ], - }, - { - code: ` - let bool1 = 1; - let bool2 = true; - for (let i = 0; bool1 || bool2; i++) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 4, - column: 25, - }, - ], - }, - { - code: ` - let bool1; - let bool2 = true; - for (let i = 0; bool1 || bool2; i++) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 4, - column: 25, - }, - ], - }, - { - code: ` - while (1) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 2, - column: 16, - }, - ], - }, - { - code: ` - while (undefined) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 2, - column: 16, - }, - ], - }, - { - code: ` - let bool = 1; - while (bool) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 3, - column: 16, - }, - ], - }, - { - code: ` - let bool; - while (bool) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 3, - column: 16, - }, - ], - }, - { - code: ` - let bool1 = 1; - let bool2 = true; - while (bool1 && bool2) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 4, - column: 16, - }, - ], - }, - { - code: ` - let bool1; - let bool2 = true; - while (bool1 && bool2) { - return; - } + code: noFormat` + if (true && 1) {} + while (false || "a") {} + (x: object) => true || false || x ? true : false; `, errors: [ - { - messageId: 'conditionErrorNullish', - line: 4, - column: 16, - }, + { messageId: 'conditionErrorNumber', line: 2, column: 13 }, + { messageId: 'conditionErrorString', line: 3, column: 25 }, + { messageId: 'conditionErrorObject', line: 4, column: 41 }, ], - }, + }), + + // check if all and only the outermost operands are checked { - code: ` - let bool1 = 1; - let bool2 = true; - while (bool1 || bool2) { - return; - } - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 4, - column: 16, - }, + options: [ + { allowString: false, allowNumber: false, allowNullableObject: false }, ], - }, - { code: ` - let bool1; - let bool2 = true; - while (bool1 || bool2) { - return; + if (('' && {}) || (0 && void 0)) { } `, errors: [ - { - messageId: 'conditionErrorNullish', - line: 4, - column: 16, - }, - ], - }, - { - code: ` - do { - return; - } while ('foo'); - `, - errors: [ - { - messageId: 'conditionErrorString', - line: 4, - column: 18, - }, + { messageId: 'conditionErrorString', line: 2, column: 14 }, + { messageId: 'conditionErrorObject', line: 2, column: 20 }, + { messageId: 'conditionErrorNumber', line: 2, column: 28 }, + { messageId: 'conditionErrorNullish', line: 2, column: 33 }, ], }, - { - code: ` - do { - return; - } while (undefined); - `, - errors: [ - { - messageId: 'conditionErrorNullish', - line: 4, - column: 18, - }, - ], - }, - { - code: ` - let bool = 1; - do { - return; - } while (bool); - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 5, - column: 18, - }, - ], - }, - { - code: ` - let bool; - do { - return; - } while (bool); - `, - errors: [ - { - messageId: 'conditionErrorAny', - line: 5, - column: 18, - }, - ], - }, - { - code: ` - let bool1 = 1; - let bool2 = true; - do { - return; - } while (bool1 && bool2); - `, - errors: [ - { - messageId: 'conditionErrorNumber', - line: 6, - column: 18, - }, - ], - }, - { - code: ` - let bool1; - let bool2 = true; - do { - return; - } while (bool1 && bool2); + + // nullish in boolean context + ...batchedSingleLineTests({ + code: noFormat` + null || {}; + undefined && []; + declare const x: null; if (x) {} + (x: undefined) => !x; + (x: T) => x ? 1 : 0; `, errors: [ - { - messageId: 'conditionErrorAny', - line: 6, - column: 18, - }, + { messageId: 'conditionErrorNullish', line: 2, column: 1 }, + { messageId: 'conditionErrorNullish', line: 3, column: 9 }, + { messageId: 'conditionErrorNullish', line: 4, column: 36 }, + { messageId: 'conditionErrorNullish', line: 5, column: 28 }, + { messageId: 'conditionErrorNullish', line: 6, column: 47 }, ], - }, - { - code: ` - let bool1 = 1; - let bool2 = true; - do { - return; - } while (bool1 || bool2); + }), + + // object in boolean context + ...batchedSingleLineTests({ + code: noFormat` + [] || 1; + ({}) && "a"; + declare const x: symbol; if (x) {} + (x: () => void) => !x; + (x: T) => x ? 1 : 0; `, errors: [ - { - messageId: 'conditionErrorNumber', - line: 6, - column: 18, - }, + { messageId: 'conditionErrorObject', line: 2, column: 1 }, + { messageId: 'conditionErrorObject', line: 3, column: 10 }, + { messageId: 'conditionErrorObject', line: 4, column: 38 }, + { messageId: 'conditionErrorObject', line: 5, column: 29 }, + { messageId: 'conditionErrorObject', line: 6, column: 37 }, ], - }, - { - code: ` - let bool1; - let bool2 = true; - do { - return; - } while (bool1 || bool2); + }), + + // string in boolean context + ...batchedSingleLineTests({ + options: [{ allowString: false }], + code: noFormat` + while ("") {} + for (; "foo";) {} + declare const x: string; if (x) {} + (x: string) => !x; + (x: T) => x ? 1 : 0; `, errors: [ - { - messageId: 'conditionErrorAny', - line: 6, - column: 18, - }, + { messageId: 'conditionErrorString', line: 2, column: 8 }, + { messageId: 'conditionErrorString', line: 3, column: 16 }, + { messageId: 'conditionErrorString', line: 4, column: 38 }, + { messageId: 'conditionErrorString', line: 5, column: 25 }, + { messageId: 'conditionErrorString', line: 6, column: 37 }, ], - }, - { - code: ` - function foo(arg: T) { - return !arg; - } + }), + + // number in boolean context + ...batchedSingleLineTests({ + options: [{ allowNumber: false }], + code: noFormat` + while (0n) {} + for (; 123;) {} + declare const x: number; if (x) {} + (x: bigint) => !x; + (x: T) => x ? 1 : 0; `, errors: [ - { - messageId: 'conditionErrorNumber', - line: 3, - column: 19, - }, - ], - }, - ...batchedSingleLineTests({ - errors: [ - { - messageId: 'conditionErrorNullableBoolean', - line: 2, - column: 48, - }, - { - messageId: 'conditionErrorNullableBoolean', - line: 3, - column: 38, - }, - { - messageId: 'conditionErrorOther', - line: 4, - column: 42, - }, + { messageId: 'conditionErrorNumber', line: 2, column: 8 }, + { messageId: 'conditionErrorNumber', line: 3, column: 16 }, + { messageId: 'conditionErrorNumber', line: 4, column: 38 }, + { messageId: 'conditionErrorNumber', line: 5, column: 25 }, + { messageId: 'conditionErrorNumber', line: 6, column: 37 }, ], - code: ` - const f1 = (x: boolean | null | undefined) => (x ? 1 : 0); - const f2 = (x?: boolean) => (x ? 1 : 0); - const f3 = (x: boolean | {}) => (x ? 1 : 0); - `, }), - { - options: [{ ignoreRhs: true }], - errors: [ - { - messageId: 'conditionErrorObject', - line: 4, - column: 27, - }, - { - messageId: 'conditionErrorObject', - line: 5, - column: 28, - }, - ], - code: ` - const obj = { x: 1 }; - const bool = false; - const objOrBool = obj || bool; - const objAndBool = obj && bool; + + // mixed `string | number` value in boolean context + ...batchedSingleLineTests({ + options: [{ allowString: true, allowNumber: true }], + code: noFormat` + declare const x: string | number; if (x) {} + (x: bigint | string) => !x; + (x: T) => x ? 1 : 0; `, - }, - { - options: [{ ignoreRhs: true }], errors: [ - { - messageId: 'conditionErrorOther', - line: 4, - column: 13, - }, - { - messageId: 'conditionErrorOther', - line: 6, - column: 13, - }, + { messageId: 'conditionErrorOther', line: 2, column: 39 }, + { messageId: 'conditionErrorOther', line: 3, column: 34 }, + { messageId: 'conditionErrorOther', line: 4, column: 55 }, ], - code: ` - const condition = () => false; - const obj = { x: 1 }; - if (condition() || obj) { - } - if (condition() && obj) { - } + }), + + // nullable boolean in boolean context + ...batchedSingleLineTests({ + options: [{ allowNullableBoolean: false }], + code: noFormat` + declare const x: boolean | null; if (x) {} + (x?: boolean) => !x; + (x: T) => x ? 1 : 0; `, - }, - { - options: [{ ignoreRhs: true }], errors: [ - { - messageId: 'conditionErrorOther', - line: 4, - column: 13, - }, - { - messageId: 'conditionErrorOther', - line: 6, - column: 13, - }, + { messageId: 'conditionErrorNullableBoolean', line: 2, column: 38 }, + { messageId: 'conditionErrorNullableBoolean', line: 3, column: 27 }, + { messageId: 'conditionErrorNullableBoolean', line: 4, column: 57 }, ], - code: ` - declare let condition: boolean; - const obj = { x: 1 }; - if (condition || obj) { - } - if (condition && obj) { - } + }), + + // nullable object in boolean context + ...batchedSingleLineTests({ + options: [{ allowNullableObject: false }], + code: noFormat` + declare const x: object | null; if (x) {} + (x?: { a: number }) => !x; + (x: T) => x ? 1 : 0; `, - }, - ...batchedSingleLineTests({ - options: [{ allowNullable: true }], errors: [ - { - messageId: 'conditionErrorNullish', - line: 2, - column: 38, - }, - { - messageId: 'conditionErrorNullableNumber', - line: 3, - column: 37, - }, - { - messageId: 'conditionErrorNullableString', - line: 4, - column: 37, - }, - { - messageId: 'conditionErrorOther', - line: 5, - column: 46, - }, + { messageId: 'conditionErrorNullableObject', line: 2, column: 37 }, + { messageId: 'conditionErrorNullableObject', line: 3, column: 33 }, + { messageId: 'conditionErrorNullableObject', line: 4, column: 52 }, ], - code: ` - const f1 = (x: null | undefined) => (x ? 1 : 0); - const f2 = (x?: number) => (x ? 1 : 0); - const f3 = (x?: string) => (x ? 1 : 0); - const f4 = (x?: string | number) => (x ? 1 : 0); - `, }), - { - errors: [ - { - messageId: 'conditionErrorOther', - line: 3, - column: 44, - }, - { - messageId: 'conditionErrorOther', - line: 4, - column: 45, - }, - ], - code: ` - type Type = { a: string }; - const f1 = (x: Type | boolean) => (x ? 1 : 0); - const f2 = (x?: Type | boolean) => (x ? 1 : 0); + + // nullable string in boolean context + ...batchedSingleLineTests({ + code: noFormat` + declare const x: string | null; if (x) {} + (x?: string) => !x; + (x: T) => x ? 1 : 0; `, - }, - ...batchedSingleLineTests({ - options: [{ allowSafe: true }], errors: [ - { - messageId: 'conditionErrorOther', - line: 2, - column: 37, - }, - { - messageId: 'conditionErrorOther', - line: 3, - column: 45, - }, - { - messageId: 'conditionErrorOther', - line: 4, - column: 45, - }, + { messageId: 'conditionErrorNullableString', line: 2, column: 37 }, + { messageId: 'conditionErrorNullableString', line: 3, column: 26 }, + { messageId: 'conditionErrorNullableString', line: 4, column: 56 }, ], - code: ` - const f1 = (x: object | string) => (x ? 1 : 0); - const f2 = (x: object | number) => (x ? 1 : 0); - const f3 = (x: number | string) => (x ? 1 : 0); - `, }), - { - options: [{ allowSafe: true }], - errors: [ - { - messageId: 'conditionErrorNumber', - line: 12, - column: 35, - }, - { - messageId: 'conditionErrorString', - line: 13, - column: 35, - }, - ], - code: ` - enum Enum1 { - A, - B, - C, - } - enum Enum2 { - A = 'A', - B = 'B', - C = 'C', - } - const f1 = (x: Enum1) => (x ? 1 : 0); - const f2 = (x: Enum2) => (x ? 1 : 0); + + // nullable number in boolean context + ...batchedSingleLineTests({ + code: noFormat` + declare const x: number | null; if (x) {} + (x?: number) => !x; + (x: T) => x ? 1 : 0; `, - }, - { - options: [{ allowNullable: true, allowSafe: true }], errors: [ - { - messageId: 'conditionErrorOther', - line: 3, - column: 44, - }, - { - messageId: 'conditionErrorOther', - line: 4, - column: 50, - }, + { messageId: 'conditionErrorNullableNumber', line: 2, column: 37 }, + { messageId: 'conditionErrorNullableNumber', line: 3, column: 26 }, + { messageId: 'conditionErrorNullableNumber', line: 4, column: 56 }, ], - code: ` - type Type = { a: string }; - const f1 = (x?: Type | string) => (x ? 1 : 0); - const f2 = (x: Type | number | null) => (x ? 1 : 0); + }), + + // any in boolean context + // TODO: when `T` is not `extends any` then the error is `conditionErrorObject` (says it's always truthy, which is false) + ...batchedSingleLineTests({ + code: noFormat` + if (x) {} + x => !x; + (x: T) => x ? 1 : 0; `, - }, - ...batchedSingleLineTests({ errors: [ - { - messageId: 'conditionErrorObject', - line: 2, - column: 32, - }, - { - messageId: 'conditionErrorNullableObject', - line: 3, - column: 41, - }, - { - messageId: 'conditionErrorNullableObject', - line: 4, - column: 48, - }, + { messageId: 'conditionErrorAny', line: 2, column: 5 }, + { messageId: 'conditionErrorAny', line: 3, column: 15 }, + { messageId: 'conditionErrorAny', line: 4, column: 34 }, ], - code: ` - const f1 = (x: { x: any }) => (x ? 1 : 0); - const f2 = (x?: { x: any }) => (x ? 1 : 0); - const f3 = (x?: { x: any } | null) => (x ? 1 : 0); - `, }), ], });