From db4b530f3f049267d679e89d9e75acfcb86faaf2 Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 12 Feb 2020 03:29:55 +0100 Subject: [PATCH] feat(eslint-plugin): [strict-boolean-expressions] refactor, add clearer error messages (#1480) --- .cspell.json | 1 + .../src/rules/strict-boolean-expressions.ts | 346 ++++++++++++---- .../rules/strict-boolean-expressions.test.ts | 381 ++++++++++++------ 3 files changed, 517 insertions(+), 211 deletions(-) diff --git a/.cspell.json b/.cspell.json index 4931ff3a5d9..33f7b70c5f2 100644 --- a/.cspell.json +++ b/.cspell.json @@ -69,6 +69,7 @@ "pluggable", "postprocess", "prettier's", + "recurse", "reimplement", "resync", "ruleset", diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts index c924045ba02..265bc05a41f 100644 --- a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts +++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts @@ -6,13 +6,6 @@ import * as ts from 'typescript'; import * as tsutils from 'tsutils'; import * as util from '../util'; -type ExpressionWithTest = - | TSESTree.ConditionalExpression - | TSESTree.DoWhileStatement - | TSESTree.ForStatement - | TSESTree.IfStatement - | TSESTree.WhileStatement; - type Options = [ { ignoreRhs?: boolean; @@ -21,7 +14,19 @@ type Options = [ }, ]; -export default util.createRule({ +type MessageId = + | 'conditionErrorOther' + | 'conditionErrorAny' + | 'conditionErrorNullish' + | 'conditionErrorNullableBoolean' + | 'conditionErrorString' + | 'conditionErrorNullableString' + | 'conditionErrorNumber' + | 'conditionErrorNullableNumber' + | 'conditionErrorObject' + | 'conditionErrorNullableObject'; + +export default util.createRule({ name: 'strict-boolean-expressions', meta: { type: 'suggestion', @@ -49,7 +54,36 @@ export default util.createRule({ }, ], messages: { - strictBooleanExpression: 'Unexpected non-boolean in conditional.', + conditionErrorOther: + 'Unexpected value in conditional. ' + + 'A boolean expression is required.', + conditionErrorAny: + 'Unexpected any value in conditional. ' + + 'An explicit comparison or type cast is required.', + conditionErrorNullish: + 'Unexpected nullish value in conditional. ' + + 'The condition is always false.', + conditionErrorNullableBoolean: + 'Unexpected nullable boolean value in conditional. ' + + 'Please handle the nullish case explicitly.', + conditionErrorString: + 'Unexpected string value in conditional. ' + + 'An explicit empty string check is required.', + conditionErrorNullableString: + 'Unexpected nullable string value in conditional. ' + + 'Please handle the nullish/empty cases explicitly.', + conditionErrorNumber: + 'Unexpected number value in conditional. ' + + 'An explicit zero/NaN check is required.', + conditionErrorNullableNumber: + 'Unexpected nullable number value in conditional. ' + + 'Please handle the nullish/zero/NaN cases explicitly.', + conditionErrorObject: + 'Unexpected object value in conditional. ' + + 'The condition is always true.', + conditionErrorNullableObject: + 'Unexpected nullable object value in conditional. ' + + 'An explicit null check is required.', }, }, defaultOptions: [ @@ -63,109 +97,247 @@ export default util.createRule({ const service = util.getParserServices(context); const checker = service.program.getTypeChecker(); + const checkedNodes = new Set(); + + return { + ConditionalExpression: checkTestExpression, + DoWhileStatement: checkTestExpression, + ForStatement: checkTestExpression, + IfStatement: checkTestExpression, + WhileStatement: checkTestExpression, + 'LogicalExpression[operator!="??"]': checkNode, + 'UnaryExpression[operator="!"]': checkUnaryLogicalExpression, + }; + + type ExpressionWithTest = + | TSESTree.ConditionalExpression + | TSESTree.DoWhileStatement + | TSESTree.ForStatement + | TSESTree.IfStatement + | TSESTree.WhileStatement; + + function checkTestExpression(node: ExpressionWithTest): void { + if (node.test == null) { + return; + } + checkNode(node.test, true); + } + + function checkUnaryLogicalExpression(node: TSESTree.UnaryExpression): void { + checkNode(node.argument, true); + } + /** - * Determines if the node is safe for boolean type + * 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. + * @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. */ - function isValidBooleanNode(node: TSESTree.Expression): boolean { - const tsNode = service.esTreeNodeToTSNodeMap.get(node); - const type = util.getConstrainedTypeAtLocation(checker, tsNode); - - if (tsutils.isTypeFlagSet(type, ts.TypeFlags.BooleanLike)) { - return true; + function checkNode(node: TSESTree.Node, isRoot = false): boolean { + // prevent checking the same node multiple times + if (checkedNodes.has(node)) { + return false; } + checkedNodes.add(node); - // Check variants of union - if (tsutils.isTypeFlagSet(type, ts.TypeFlags.Union)) { - let hasBoolean = false; - for (const ty of (type as ts.UnionType).types) { - if (tsutils.isTypeFlagSet(ty, ts.TypeFlags.BooleanLike)) { - hasBoolean = true; - continue; + // for logical operator, we also 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 ( - tsutils.isTypeFlagSet(ty, ts.TypeFlags.Null) || - tsutils.isTypeFlagSet(ty, ts.TypeFlags.Undefined) - ) { - if (!options.allowNullable) { - return false; - } - continue; + } + // 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; } + } + } - if ( - !tsutils.isTypeFlagSet(ty, ts.TypeFlags.StringLike) && - !tsutils.isTypeFlagSet(ty, ts.TypeFlags.NumberLike) - ) { - if (options.allowSafe) { - hasBoolean = true; - continue; - } - } + const tsNode = service.esTreeNodeToTSNodeMap.get(node); + const type = util.getConstrainedTypeAtLocation(checker, tsNode); + let messageId: MessageId | undefined; + + const types = inspectVariantTypes(tsutils.unionTypeParts(type)); - return false; + const is = (...wantedTypes: readonly VariantType[]): boolean => + types.size === wantedTypes.length && + wantedTypes.every(type => types.has(type)); + + // boolean + if (is('boolean')) { + // boolean is always okay + return false; + } + // never + if (is('never')) { + // never is always okay + return false; + } + // nullish + else if (is('nullish')) { + // condition is always false + messageId = 'conditionErrorNullish'; + } + // nullable boolean + else if (is('nullish', 'boolean')) { + if (!options.allowNullable) { + messageId = 'conditionErrorNullableBoolean'; + } + } + // string + else if (is('string')) { + messageId = 'conditionErrorString'; + } + // nullable string + else if (is('nullish', 'string')) { + messageId = 'conditionErrorNullableString'; + } + // number + else if (is('number')) { + messageId = 'conditionErrorNumber'; + } + // nullable number + else if (is('nullish', 'number')) { + messageId = 'conditionErrorNullableNumber'; + } + // object + else if (is('object')) { + // condition is always true + if (!options.allowSafe) { + messageId = 'conditionErrorObject'; + } + } + // nullable object + else if (is('nullish', 'object')) { + if (!options.allowSafe || !options.allowNullable) { + messageId = 'conditionErrorNullableObject'; } - return hasBoolean; + } + // 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'; + } + } + // any + else if (is('any')) { + messageId = 'conditionErrorAny'; + } + // other + else { + messageId = 'conditionErrorOther'; + } + + if (messageId != null) { + context.report({ node, messageId }); + return true; } return false; } + /** The types we care about */ + type VariantType = + | 'nullish' + | 'boolean' + | 'string' + | 'number' + | 'object' + | 'any' + | 'never'; + /** - * Asserts that a testable expression contains a boolean, reports otherwise. - * Filters all LogicalExpressions to prevent some duplicate reports. + * Check union variants for the types we care about */ - function assertTestExpressionContainsBoolean( - node: ExpressionWithTest, - ): void { + function inspectVariantTypes(types: ts.Type[]): Set { + const variantTypes = new Set(); + if ( - node.test !== null && - node.test.type !== AST_NODE_TYPES.LogicalExpression && - !isValidBooleanNode(node.test) + types.some(type => + tsutils.isTypeFlagSet( + type, + ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.VoidLike, + ), + ) ) { - reportNode(node.test); + variantTypes.add('nullish'); } - } - /** - * Asserts that a logical expression contains a boolean, reports otherwise. - */ - function assertLocalExpressionContainsBoolean( - node: TSESTree.LogicalExpression, - ): void { if ( - !isValidBooleanNode(node.left) || - (!options.ignoreRhs && !isValidBooleanNode(node.right)) + types.some(type => + tsutils.isTypeFlagSet(type, ts.TypeFlags.BooleanLike), + ) ) { - reportNode(node); + variantTypes.add('boolean'); } - } - /** - * Asserts that a unary expression contains a boolean, reports otherwise. - */ - function assertUnaryExpressionContainsBoolean( - node: TSESTree.UnaryExpression, - ): void { - if (!isValidBooleanNode(node.argument)) { - reportNode(node.argument); + if ( + types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.StringLike)) + ) { + variantTypes.add('string'); } - } - /** - * Reports an offending node in context. - */ - function reportNode(node: TSESTree.Node): void { - context.report({ node, messageId: 'strictBooleanExpression' }); - } + if ( + types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.NumberLike)) + ) { + variantTypes.add('number'); + } - return { - ConditionalExpression: assertTestExpressionContainsBoolean, - DoWhileStatement: assertTestExpressionContainsBoolean, - ForStatement: assertTestExpressionContainsBoolean, - IfStatement: assertTestExpressionContainsBoolean, - WhileStatement: assertTestExpressionContainsBoolean, - 'LogicalExpression[operator!="??"]': assertLocalExpressionContainsBoolean, - 'UnaryExpression[operator="!"]': assertUnaryExpressionContainsBoolean, - }; + if ( + types.some( + type => + !tsutils.isTypeFlagSet( + type, + ts.TypeFlags.Null | + ts.TypeFlags.Undefined | + ts.TypeFlags.VoidLike | + ts.TypeFlags.BooleanLike | + ts.TypeFlags.StringLike | + ts.TypeFlags.NumberLike | + ts.TypeFlags.Any | + ts.TypeFlags.Unknown | + ts.TypeFlags.Never, + ), + ) + ) { + variantTypes.add('object'); + } + + if ( + types.some(type => + tsutils.isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown), + ) + ) { + variantTypes.add('any'); + } + + if (types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.Never))) { + variantTypes.add('never'); + } + + return variantTypes; + } }, }); 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 cf6dbd5cd36..d3d5700c5a7 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -1,5 +1,9 @@ import rule from '../../src/rules/strict-boolean-expressions'; -import { RuleTester, getFixturesRootDir } from '../RuleTester'; +import { + RuleTester, + getFixturesRootDir, + batchedSingleLineTests, +} from '../RuleTester'; const rootPath = getFixturesRootDir(); @@ -158,13 +162,13 @@ ruleTester.run('strict-boolean-expressions', rule, { { options: [{ ignoreRhs: true }], code: ` - const obj = {}; + const obj = { x: 1 }; const bool = false; const boolOrObj = bool || obj; const boolAndObj = bool && obj; `, }, - { + ...batchedSingleLineTests({ options: [{ allowNullable: true }], code: ` const f1 = (x?: boolean) => x ? 1 : 0; @@ -172,51 +176,59 @@ ruleTester.run('strict-boolean-expressions', rule, { const f3 = (x?: true | null) => x ? 1 : 0; const f4 = (x?: false) => x ? 1 : 0; `, - }, + }), ` declare const x: string | null; y = x ?? 'foo'; `, - { + ...batchedSingleLineTests({ options: [{ allowSafe: true }], code: ` - type TestType = { a: string; }; - const f1 = (x: boolean | TestType) => x ? 1 : 0; - const f2 = (x: true | TestType) => x ? 1 : 0; - const f3 = (x: TestType | false) => x ? 1 : 0; + 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; `, - }, - { + }), + ...batchedSingleLineTests({ options: [{ allowNullable: true, allowSafe: true }], code: ` - type TestType = { a: string; }; - type TestType2 = { b: number; }; - const f1 = (x?: boolean | TestType) => x ? 1 : 0; - const f2 = (x: TestType | TestType2 | null) => x ? 1 : 0; - const f3 = (x?: TestType | TestType2 | null) => x ? 1 : 0; - const f4 = (x?: TestType2 | true) => x ? 1 : 0; + 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; `, - }, - { + }), + ...batchedSingleLineTests({ options: [{ allowNullable: true, allowSafe: true, ignoreRhs: true }], code: ` - type TestType = { foo? : { bar?: string; }; }; - const f1 = (x?: TestType) => x && x.foo && x.foo.bar + const f1 = (x?: { a: null }) => x && x.foo && x.foo.bar const f2 = (g?: (x: number) => number) => g && g(1) `, - }, + }), + ` + declare let x: never; + if (x) {} + `, + ...batchedSingleLineTests({ + code: ` + 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) {} } + `, + }), ], invalid: [ { code: ` - let val = 1; + let val = "foo"; let bool = !val; `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorString', line: 3, column: 21, }, @@ -229,7 +241,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 3, column: 21, }, @@ -242,9 +254,9 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 3, - column: 20, + column: 28, }, ], }, @@ -255,9 +267,9 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 3, - column: 20, + column: 28, }, ], }, @@ -268,9 +280,9 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 3, - column: 20, + column: 28, }, ], }, @@ -281,9 +293,34 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorAny', line: 3, - column: 20, + 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, }, ], }, @@ -295,7 +332,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 2, column: 13, }, @@ -309,7 +346,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 2, column: 13, }, @@ -317,14 +354,14 @@ ruleTester.run('strict-boolean-expressions', rule, { }, { code: ` - let item = 1; + let item = "foo"; if (item) { return; } `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorString', line: 3, column: 13, }, @@ -339,7 +376,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 3, column: 13, }, @@ -355,9 +392,9 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 4, - column: 13, + column: 22, }, ], }, @@ -371,7 +408,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 4, column: 13, }, @@ -387,7 +424,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 4, column: 13, }, @@ -403,9 +440,9 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 4, - column: 13, + column: 22, }, ], }, @@ -419,7 +456,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 4, column: 13, }, @@ -435,7 +472,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 4, column: 13, }, @@ -443,11 +480,11 @@ ruleTester.run('strict-boolean-expressions', rule, { }, { code: ` - const bool = 1 ? true : false; + const bool = "foo" ? true : false; `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorString', line: 2, column: 22, }, @@ -459,7 +496,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 2, column: 22, }, @@ -472,7 +509,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 3, column: 22, }, @@ -485,7 +522,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 3, column: 22, }, @@ -499,7 +536,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 4, column: 22, }, @@ -513,9 +550,9 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 4, - column: 22, + column: 31, }, ], }, @@ -527,9 +564,9 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 4, - column: 22, + column: 31, }, ], }, @@ -541,7 +578,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 4, column: 22, }, @@ -555,9 +592,9 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 4, - column: 22, + column: 31, }, ], }, @@ -569,9 +606,9 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 4, - column: 22, + column: 31, }, ], }, @@ -583,7 +620,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 2, column: 25, }, @@ -597,7 +634,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 2, column: 25, }, @@ -612,7 +649,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 3, column: 25, }, @@ -627,7 +664,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 3, column: 25, }, @@ -635,7 +672,7 @@ ruleTester.run('strict-boolean-expressions', rule, { }, { code: ` - let bool1 = 1; + let bool1 = "foo"; let bool2 = true; for (let i = 0; bool1 && bool2; i++) { return; @@ -643,7 +680,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorString', line: 4, column: 25, }, @@ -659,7 +696,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 4, column: 25, }, @@ -675,7 +712,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 4, column: 25, }, @@ -691,7 +728,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 4, column: 25, }, @@ -705,7 +742,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 2, column: 16, }, @@ -719,7 +756,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 2, column: 16, }, @@ -734,7 +771,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 3, column: 16, }, @@ -749,7 +786,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 3, column: 16, }, @@ -765,7 +802,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 4, column: 16, }, @@ -781,7 +818,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 4, column: 16, }, @@ -797,7 +834,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 4, column: 16, }, @@ -813,7 +850,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 4, column: 16, }, @@ -823,11 +860,11 @@ ruleTester.run('strict-boolean-expressions', rule, { code: ` do { return; - } while (1); + } while ("foo"); `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorString', line: 4, column: 18, }, @@ -841,7 +878,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 4, column: 18, }, @@ -856,7 +893,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 5, column: 18, }, @@ -871,7 +908,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorAny', line: 5, column: 18, }, @@ -887,7 +924,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 6, column: 18, }, @@ -903,7 +940,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorAny', line: 6, column: 18, }, @@ -919,7 +956,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 6, column: 18, }, @@ -935,7 +972,7 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorAny', line: 6, column: 18, }, @@ -947,26 +984,26 @@ ruleTester.run('strict-boolean-expressions', rule, { `, errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 2, column: 58, }, ], }, - { + ...batchedSingleLineTests({ errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullableBoolean', line: 2, - column: 55, + column: 47, }, { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullableBoolean', line: 3, column: 37, }, { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorOther', line: 4, column: 41, }, @@ -976,83 +1013,155 @@ ruleTester.run('strict-boolean-expressions', rule, { 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; + `, }, { options: [{ ignoreRhs: true }], errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorOther', line: 4, - column: 19, + column: 13, }, { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorOther', line: 5, - column: 20, + column: 13, }, ], code: ` -const obj = {}; -const bool = false; -const objOrBool = obj || bool; -const objAndBool = obj && bool; -`, + const condition = () => false; + const obj = { x: 1 }; + if (condition() || obj) {} + if (condition() && obj) {} + `, }, { + options: [{ ignoreRhs: true }], + errors: [ + { + messageId: 'conditionErrorOther', + line: 4, + column: 13, + }, + { + messageId: 'conditionErrorOther', + line: 5, + column: 13, + }, + ], + code: ` + declare let condition: boolean; + const obj = { x: 1 }; + if (condition || obj) {} + if (condition && obj) {} + `, + }, + ...batchedSingleLineTests({ options: [{ allowNullable: true }], errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullish', line: 2, - column: 44, + column: 37, }, { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorNullableNumber', line: 3, - column: 35, + column: 36, + }, + { + messageId: 'conditionErrorNullableString', + line: 4, + column: 36, + }, + { + messageId: 'conditionErrorOther', + line: 5, + column: 45, }, ], code: ` - const f = (x: null | undefined) => x ? 1 : 0; - const f = (x?: number) => x ? 1 : 0; + 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; `, - }, + }), { - options: [{ allowSafe: true }], errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorOther', line: 3, - column: 42, + column: 43, }, { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorOther', line: 4, - column: 42, + column: 44, + }, + ], + code: ` + type Type = { a: string; }; + const f1 = (x: Type | boolean) => x ? 1 : 0; + const f2 = (x?: Type | boolean) => x ? 1 : 0; + `, + }, + ...batchedSingleLineTests({ + options: [{ allowSafe: true }], + errors: [ + { + messageId: 'conditionErrorOther', + line: 2, + column: 36, }, { - messageId: 'strictBooleanExpression', - line: 5, + messageId: 'conditionErrorOther', + line: 3, + column: 44, + }, + { + messageId: 'conditionErrorOther', + line: 4, column: 44, }, ], code: ` - type Type = { a: string; }; - const f1 = (x: Type | string) => x ? 1 : 0; - const f2 = (x: Type | number) => x ? 1 : 0; + 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: 'strictBooleanExpression', + messageId: 'conditionErrorNumber', line: 8, column: 34, }, { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorString', line: 9, column: 34, }, @@ -1072,12 +1181,12 @@ const objAndBool = obj && bool; options: [{ allowNullable: true, allowSafe: true }], errors: [ { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorOther', line: 3, column: 43, }, { - messageId: 'strictBooleanExpression', + messageId: 'conditionErrorOther', line: 4, column: 49, }, @@ -1088,5 +1197,29 @@ const objAndBool = obj && bool; const f2 = (x: Type | number | null) => x ? 1 : 0; `, }, + ...batchedSingleLineTests({ + errors: [ + { + messageId: 'conditionErrorObject', + line: 2, + column: 31, + }, + { + messageId: 'conditionErrorNullableObject', + line: 3, + column: 40, + }, + { + messageId: 'conditionErrorNullableObject', + line: 4, + column: 47, + }, + ], + 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; + `, + }), ], });