diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 22a9fed5ba65..4763f54f629a 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -11,6 +11,7 @@ import { isBooleanLiteralType, isLiteralType, getCallSignaturesOfType, + isStrictCompilerOptionEnabled, } from 'tsutils'; import { createRule, @@ -21,6 +22,9 @@ import { NullThrowsReasons, } from '../util'; +const typeContainsFlag = (type: ts.Type, flag: ts.TypeFlags) => { + return unionTypeParts(type).some(t => isTypeFlagSet(t, flag)); +}; // Truthiness utilities // #region const isTruthyLiteral = (type: ts.Type): boolean => @@ -73,6 +77,7 @@ export type MessageId = | 'neverNullish' | 'alwaysNullish' | 'literalBooleanExpression' + | 'noOverlapBooleanExpression' | 'never' | 'neverOptionalChain'; export default createRule({ @@ -117,6 +122,8 @@ export default createRule({ 'Unnecessary conditional, left-hand side of `??` operator is always `null` or `undefined`', literalBooleanExpression: 'Unnecessary conditional, both sides of the expression are literal values', + noOverlapBooleanExpression: + 'Unnecessary conditional, the types have no overlap', never: 'Unnecessary conditional, value is `never`', neverOptionalChain: 'Unnecessary optional chain on a non-nullish value', }, @@ -135,6 +142,7 @@ export default createRule({ const service = getParserServices(context); const checker = service.program.getTypeChecker(); const sourceCode = context.getSourceCode(); + const compilerOptions = service.program.getCompilerOptions(); function getNodeType(node: TSESTree.Expression): ts.Type { const tsNode = service.esTreeNodeToTSNodeMap.get(node); @@ -205,6 +213,9 @@ export default createRule({ * * NOTE: It's also unnecessary if the types that don't overlap at all * but that case is handled by the Typescript compiler itself. + * Known exceptions: + * * https://github.com/microsoft/TypeScript/issues/32627 + * * https://github.com/microsoft/TypeScript/issues/37160 (handled) */ const BOOL_OPERATORS = new Set([ '<', @@ -219,12 +230,30 @@ export default createRule({ function checkIfBinaryExpressionIsNecessaryConditional( node: TSESTree.BinaryExpression, ): void { - if ( - BOOL_OPERATORS.has(node.operator) && - isLiteral(getNodeType(node.left)) && - isLiteral(getNodeType(node.right)) - ) { + if (!BOOL_OPERATORS.has(node.operator)) { + return; + } + const leftType = getNodeType(node.left); + const rightType = getNodeType(node.right); + if (isLiteral(leftType) && isLiteral(rightType)) { context.report({ node, messageId: 'literalBooleanExpression' }); + return; + } + // Workaround for https://github.com/microsoft/TypeScript/issues/37160 + if (isStrictCompilerOptionEnabled(compilerOptions, 'strictNullChecks')) { + const UNDEFINED = ts.TypeFlags.Undefined; + const NULL = ts.TypeFlags.Null; + if ( + (leftType.flags === UNDEFINED && + !typeContainsFlag(rightType, UNDEFINED)) || + (rightType.flags === UNDEFINED && + !typeContainsFlag(leftType, UNDEFINED)) || + (leftType.flags === NULL && !typeContainsFlag(rightType, NULL)) || + (rightType.flags === NULL && !typeContainsFlag(leftType, NULL)) + ) { + context.report({ node, messageId: 'noOverlapBooleanExpression' }); + return; + } } } diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index 0728b9202b9f..e43ea21e8c78 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -87,6 +87,11 @@ function test(t: T | []) { function test(a: string) { return a === "a" }`, + ` +function test(a?: string) { + const t1 = a === undefined; + const t3 = undefined === a; +}`, /** * Predicate functions @@ -304,6 +309,29 @@ if (x === Foo.a) {} `, errors: [ruleError(8, 5, 'literalBooleanExpression')], }, + // Workaround https://github.com/microsoft/TypeScript/issues/37160 + { + code: ` +function test(a: string) { + const t1 = a !== undefined; + const t3 = undefined === a; +}`, + errors: [ + ruleError(3, 14, 'noOverlapBooleanExpression'), + ruleError(4, 14, 'noOverlapBooleanExpression'), + ], + }, + { + code: ` +function test(a?: string) { + const t1 = a === null; + const t3 = null !== a; +}`, + errors: [ + ruleError(3, 14, 'noOverlapBooleanExpression'), + ruleError(4, 14, 'noOverlapBooleanExpression'), + ], + }, // Nullish coalescing operator { code: `