From c7adb29b9893613f37996597a2cc0f0eaf00f162 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Mon, 21 Oct 2024 23:55:07 -0600 Subject: [PATCH 1/7] no-unnecessary-condition: Improve error message for literal comparisons --- .../src/rules/no-unnecessary-condition.ts | 117 +++++-- .../rules/no-unnecessary-condition.test.ts | 301 ++++++++++++------ 2 files changed, 303 insertions(+), 115 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index ed963aa4703b..a61296736a2c 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -63,13 +63,80 @@ const isPossiblyNullish = (type: ts.Type): boolean => const isAlwaysNullish = (type: ts.Type): boolean => tsutils.unionTypeParts(type).every(isNullishType); -// isLiteralType only covers numbers and strings, this is a more exhaustive check. -const isLiteral = (type: ts.Type): boolean => - tsutils.isBooleanLiteralType(type) || - type.flags === ts.TypeFlags.Undefined || - type.flags === ts.TypeFlags.Null || - type.flags === ts.TypeFlags.Void || - type.isLiteral(); +function toLiteralValue( + type: ts.Type, +): + | { value: bigint | boolean | number | string | null | undefined } + | undefined { + // type.isLiteral() only covers numbers/bigints and strings, hence the rest of the branches. + if (tsutils.isBooleanLiteralType(type)) { + return { value: type.value }; + } else if (type.flags === ts.TypeFlags.Undefined) { + return { value: undefined }; + } else if (type.flags === ts.TypeFlags.Null) { + return { value: null }; + } else if (type.isLiteral()) { + if (typeof type.value === 'string' || typeof type.value === 'number') { + return { value: type.value }; + } + return { value: pseudoBigIntToBigInt(type.value) }; + } + + return undefined; +} + +function pseudoBigIntToBigInt(value: ts.PseudoBigInt): bigint { + return BigInt((value.negative ? '-' : '') + value.base10Value); +} + +const BOOL_OPERATORS = new Set([ + '<', + '>', + '<=', + '>=', + '==', + '===', + '!=', + '!==', +] as const); + +type BoolOperator = typeof BOOL_OPERATORS extends Set ? T : never; + +function isBoolOperator(operator: string): operator is BoolOperator { + return (BOOL_OPERATORS as Set).has(operator); +} + +function booleanComparison( + left: unknown, + operator: BoolOperator, + right: unknown, +): boolean { + switch (operator) { + case '!=': + // eslint-disable-next-line eqeqeq -- intentionally comparing with loose equality + return left != right; + case '!==': + return left !== right; + case '<': + // @ts-expect-error: we don't care if the comparison seems unintentional. + return left < right; + case '<=': + // @ts-expect-error: we don't care if the comparison seems unintentional. + return left <= right; + case '==': + // eslint-disable-next-line eqeqeq -- intentionally comparing with loose equality + return left == right; + case '===': + return left === right; + case '>': + // @ts-expect-error: we don't care if the comparison seems unintentional. + return left > right; + case '>=': + // @ts-expect-error: we don't care if the comparison seems unintentional. + return left >= right; + } +} + // #endregion export type Options = [ @@ -115,7 +182,7 @@ export default createRule({ alwaysTruthyFunc: 'This callback should return a conditional, but return is always truthy.', literalBooleanExpression: - 'Unnecessary conditional, both sides of the expression are literal values.', + 'Unnecessary conditional, comparison is always {{trueOrFalse}}. Both sides of the comparison always have a literal type.', never: 'Unnecessary conditional, value is `never`.', neverNullish: 'Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.', @@ -371,19 +438,6 @@ export default createRule({ * - https://github.com/microsoft/TypeScript/issues/32627 * - https://github.com/microsoft/TypeScript/issues/37160 (handled) */ - const BOOL_OPERATORS = new Set([ - '<', - '>', - '<=', - '>=', - '==', - '===', - '!=', - '!==', - ] as const); - type BoolOperator = Parameters[0]; - const isBoolOperator = (operator: string): operator is BoolOperator => - (BOOL_OPERATORS as Set).has(operator); function checkIfBoolExpressionIsNecessaryConditional( node: TSESTree.Node, left: TSESTree.Node, @@ -392,10 +446,27 @@ export default createRule({ ): void { const leftType = getConstrainedTypeAtLocation(services, left); const rightType = getConstrainedTypeAtLocation(services, right); - if (isLiteral(leftType) && isLiteral(rightType)) { - context.report({ node, messageId: 'literalBooleanExpression' }); + + const leftLiteralValue = toLiteralValue(leftType); + const rightLiteralValue = toLiteralValue(rightType); + + if (leftLiteralValue != null && rightLiteralValue != null) { + const conditionIsTrue = booleanComparison( + leftLiteralValue.value, + operator, + rightLiteralValue.value, + ); + + context.report({ + node, + messageId: 'literalBooleanExpression', + data: { + trueOrFalse: conditionIsTrue ? 'true' : 'false', + }, + }); return; } + // Workaround for https://github.com/microsoft/TypeScript/issues/37160 if (isStrictNullChecks) { const UNDEFINED = ts.TypeFlags.Undefined; 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 629228f4acc7..bc6b89ea8034 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -30,16 +30,6 @@ const optionsWithExactOptionalPropertyTypes = { tsconfigRootDir: rootPath, }; -const ruleError = ( - line: number, - column: number, - messageId: MessageId, -): TestCaseError => ({ - column, - line, - messageId, -}); - const necessaryConditionTest = (condition: string): string => ` declare const b1: ${condition}; declare const b2: boolean; @@ -51,7 +41,7 @@ const unnecessaryConditionTest = ( messageId: MessageId, ): InvalidTestCase => ({ code: necessaryConditionTest(condition), - errors: [ruleError(4, 12, messageId)], + errors: [{ column: 12, line: 4, messageId }], }); ruleTester.run('no-unnecessary-condition', rule, { @@ -1017,16 +1007,21 @@ switch (b1) { } `, errors: [ - ruleError(4, 12, 'alwaysTruthy'), - ruleError(5, 12, 'alwaysTruthy'), - ruleError(6, 5, 'alwaysTruthy'), - ruleError(8, 11, 'alwaysTruthy'), - ruleError(10, 8, 'alwaysTruthy'), - ruleError(11, 14, 'alwaysTruthy'), - ruleError(12, 17, 'alwaysTruthy'), - ruleError(15, 12, 'alwaysTruthy'), - ruleError(16, 18, 'alwaysTruthy'), - ruleError(18, 8, 'literalBooleanExpression'), + { column: 12, line: 4, messageId: 'alwaysTruthy' }, + { column: 12, line: 5, messageId: 'alwaysTruthy' }, + { column: 5, line: 6, messageId: 'alwaysTruthy' }, + { column: 11, line: 8, messageId: 'alwaysTruthy' }, + { column: 8, line: 10, messageId: 'alwaysTruthy' }, + { column: 14, line: 11, messageId: 'alwaysTruthy' }, + { column: 17, line: 12, messageId: 'alwaysTruthy' }, + { column: 12, line: 15, messageId: 'alwaysTruthy' }, + { column: 18, line: 16, messageId: 'alwaysTruthy' }, + { + column: 8, + data: { trueOrFalse: 'true' }, + line: 18, + messageId: 'literalBooleanExpression', + }, ], output: null, }, @@ -1054,9 +1049,9 @@ if (b1 || b2 || true) { } `, errors: [ - ruleError(4, 5, 'alwaysTruthy'), - ruleError(6, 11, 'alwaysFalsy'), - ruleError(8, 17, 'alwaysTruthy'), + { column: 5, line: 4, messageId: 'alwaysTruthy' }, + { column: 11, line: 6, messageId: 'alwaysFalsy' }, + { column: 17, line: 8, messageId: 'alwaysTruthy' }, ], output: null, }, @@ -1068,7 +1063,7 @@ function test(t: T) { return t ? 'yes' : 'no'; } `, - errors: [ruleError(3, 10, 'alwaysTruthy')], + errors: [{ column: 10, line: 3, messageId: 'alwaysTruthy' }], output: null, }, { @@ -1077,7 +1072,7 @@ function test(t: T) { return t ? 'yes' : 'no'; } `, - errors: [ruleError(3, 10, 'alwaysFalsy')], + errors: [{ column: 10, line: 3, messageId: 'alwaysFalsy' }], output: null, }, { @@ -1086,7 +1081,7 @@ function test(t: T) { return t ? 'yes' : 'no'; } `, - errors: [ruleError(3, 10, 'alwaysTruthy')], + errors: [{ column: 10, line: 3, messageId: 'alwaysTruthy' }], output: null, }, @@ -1097,16 +1092,129 @@ function test(a: 'a') { return a === 'a'; } `, - errors: [ruleError(3, 10, 'literalBooleanExpression')], + errors: [ + { + column: 10, + data: { + trueOrFalse: 'true', + }, + line: 3, + messageId: 'literalBooleanExpression', + }, + ], output: null, }, { code: ` +declare const a: '34'; +declare const b: '56'; +a > b; + `, + errors: [ + { + data: { + trueOrFalse: 'false', + }, + line: 4, + messageId: 'literalBooleanExpression', + }, + ], + }, + { + code: ` const y = 1; if (y === 0) { } `, - errors: [ruleError(3, 5, 'literalBooleanExpression')], + errors: [ + { + data: { trueOrFalse: 'false' }, + line: 3, + messageId: 'literalBooleanExpression', + }, + ], + output: null, + }, + { + code: ` +// @ts-expect-error +if (1 == '1') { +} + `, + errors: [ + { + data: { trueOrFalse: 'true' }, + line: 3, + messageId: 'literalBooleanExpression', + }, + ], + output: null, + }, + { + code: ` +2.3 > 2.3; + `, + errors: [ + { + data: { trueOrFalse: 'false' }, + line: 2, + messageId: 'literalBooleanExpression', + }, + ], + output: null, + }, + { + code: ` +2.3 >= 2.3; + `, + errors: [ + { + data: { trueOrFalse: 'true' }, + line: 2, + messageId: 'literalBooleanExpression', + }, + ], + output: null, + }, + { + code: ` +2n < 2n; + `, + errors: [ + { + data: { trueOrFalse: 'false' }, + line: 2, + messageId: 'literalBooleanExpression', + }, + ], + output: null, + }, + { + code: ` +2n <= 2n; + `, + errors: [ + { + data: { trueOrFalse: 'true' }, + line: 2, + messageId: 'literalBooleanExpression', + }, + ], + output: null, + }, + { + code: ` +// @ts-expect-error +if (1 == '2') { +} + `, + errors: [ + { + data: { trueOrFalse: 'false' }, + line: 3, + messageId: 'literalBooleanExpression', + }, + ], output: null, }, { @@ -1120,7 +1228,16 @@ const x = Foo.a; if (x === Foo.a) { } `, - errors: [ruleError(8, 5, 'literalBooleanExpression')], + errors: [ + { + column: 5, + data: { + trueOrFalse: 'true', + }, + line: 8, + messageId: 'literalBooleanExpression', + }, + ], output: null, }, // Workaround https://github.com/microsoft/TypeScript/issues/37160 @@ -1138,14 +1255,14 @@ function test(a: string) { } `, errors: [ - ruleError(3, 14, 'noOverlapBooleanExpression'), - ruleError(4, 14, 'noOverlapBooleanExpression'), - ruleError(5, 14, 'noOverlapBooleanExpression'), - ruleError(6, 14, 'noOverlapBooleanExpression'), - ruleError(7, 14, 'noOverlapBooleanExpression'), - ruleError(8, 14, 'noOverlapBooleanExpression'), - ruleError(9, 14, 'noOverlapBooleanExpression'), - ruleError(10, 14, 'noOverlapBooleanExpression'), + { column: 14, line: 3, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 4, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 5, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 6, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 7, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 8, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 9, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 10, messageId: 'noOverlapBooleanExpression' }, ], output: null, }, @@ -1163,10 +1280,10 @@ function test(a?: string) { } `, errors: [ - ruleError(7, 14, 'noOverlapBooleanExpression'), - ruleError(8, 14, 'noOverlapBooleanExpression'), - ruleError(9, 14, 'noOverlapBooleanExpression'), - ruleError(10, 14, 'noOverlapBooleanExpression'), + { column: 14, line: 7, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 8, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 9, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 10, messageId: 'noOverlapBooleanExpression' }, ], output: null, }, @@ -1184,10 +1301,10 @@ function test(a: null | string) { } `, errors: [ - ruleError(3, 14, 'noOverlapBooleanExpression'), - ruleError(4, 14, 'noOverlapBooleanExpression'), - ruleError(5, 14, 'noOverlapBooleanExpression'), - ruleError(6, 14, 'noOverlapBooleanExpression'), + { column: 14, line: 3, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 4, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 5, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 6, messageId: 'noOverlapBooleanExpression' }, ], output: null, }, @@ -1213,22 +1330,22 @@ function test(a: T) { } `, errors: [ - ruleError(3, 14, 'noOverlapBooleanExpression'), - ruleError(4, 14, 'noOverlapBooleanExpression'), - ruleError(5, 14, 'noOverlapBooleanExpression'), - ruleError(6, 14, 'noOverlapBooleanExpression'), - ruleError(7, 14, 'noOverlapBooleanExpression'), - ruleError(8, 14, 'noOverlapBooleanExpression'), - ruleError(9, 14, 'noOverlapBooleanExpression'), - ruleError(10, 14, 'noOverlapBooleanExpression'), - ruleError(11, 14, 'noOverlapBooleanExpression'), - ruleError(12, 15, 'noOverlapBooleanExpression'), - ruleError(13, 15, 'noOverlapBooleanExpression'), - ruleError(14, 15, 'noOverlapBooleanExpression'), - ruleError(15, 15, 'noOverlapBooleanExpression'), - ruleError(16, 15, 'noOverlapBooleanExpression'), - ruleError(17, 15, 'noOverlapBooleanExpression'), - ruleError(18, 15, 'noOverlapBooleanExpression'), + { column: 14, line: 3, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 4, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 5, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 6, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 7, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 8, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 9, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 10, messageId: 'noOverlapBooleanExpression' }, + { column: 14, line: 11, messageId: 'noOverlapBooleanExpression' }, + { column: 15, line: 12, messageId: 'noOverlapBooleanExpression' }, + { column: 15, line: 13, messageId: 'noOverlapBooleanExpression' }, + { column: 15, line: 14, messageId: 'noOverlapBooleanExpression' }, + { column: 15, line: 15, messageId: 'noOverlapBooleanExpression' }, + { column: 15, line: 16, messageId: 'noOverlapBooleanExpression' }, + { column: 15, line: 17, messageId: 'noOverlapBooleanExpression' }, + { column: 15, line: 18, messageId: 'noOverlapBooleanExpression' }, ], output: null, }, @@ -1239,7 +1356,7 @@ function test(a: string) { return a ?? 'default'; } `, - errors: [ruleError(3, 10, 'neverNullish')], + errors: [{ column: 10, line: 3, messageId: 'neverNullish' }], output: null, }, { @@ -1248,7 +1365,7 @@ function test(a: string | false) { return a ?? 'default'; } `, - errors: [ruleError(3, 10, 'neverNullish')], + errors: [{ column: 10, line: 3, messageId: 'neverNullish' }], output: null, }, { @@ -1257,7 +1374,7 @@ function test(a: T) { return a ?? 'default'; } `, - errors: [ruleError(3, 10, 'neverNullish')], + errors: [{ column: 10, line: 3, messageId: 'neverNullish' }], output: null, }, // nullish + array index without optional chaining @@ -1267,7 +1384,7 @@ function test(a: { foo: string }[]) { return a[0].foo ?? 'default'; } `, - errors: [ruleError(3, 10, 'neverNullish')], + errors: [{ column: 10, line: 3, messageId: 'neverNullish' }], output: null, }, { @@ -1276,7 +1393,7 @@ function test(a: null) { return a ?? 'default'; } `, - errors: [ruleError(3, 10, 'alwaysNullish')], + errors: [{ column: 10, line: 3, messageId: 'alwaysNullish' }], output: null, }, { @@ -1285,7 +1402,7 @@ function test(a: null[]) { return a[0] ?? 'default'; } `, - errors: [ruleError(3, 10, 'alwaysNullish')], + errors: [{ column: 10, line: 3, messageId: 'alwaysNullish' }], output: null, }, { @@ -1294,7 +1411,7 @@ function test(a: T) { return a ?? 'default'; } `, - errors: [ruleError(3, 10, 'alwaysNullish')], + errors: [{ column: 10, line: 3, messageId: 'alwaysNullish' }], output: null, }, { @@ -1303,7 +1420,7 @@ function test(a: never) { return a ?? 'default'; } `, - errors: [ruleError(3, 10, 'never')], + errors: [{ column: 10, line: 3, messageId: 'never' }], output: null, }, { @@ -1312,7 +1429,7 @@ function test(num: T[K]) { num ?? 'default'; } `, - errors: [ruleError(3, 3, 'neverNullish')], + errors: [{ column: 3, line: 3, messageId: 'neverNullish' }], output: null, }, // Predicate functions @@ -1337,11 +1454,11 @@ function nothing3(x: [string, string]) { } `, errors: [ - ruleError(2, 24, 'alwaysTruthy'), - ruleError(4, 10, 'alwaysFalsy'), - ruleError(9, 25, 'alwaysFalsy'), - ruleError(13, 25, 'alwaysFalsy'), - ruleError(17, 25, 'alwaysFalsy'), + { column: 24, line: 2, messageId: 'alwaysTruthy' }, + { column: 10, line: 4, messageId: 'alwaysFalsy' }, + { column: 25, line: 9, messageId: 'alwaysFalsy' }, + { column: 25, line: 13, messageId: 'alwaysFalsy' }, + { column: 25, line: 17, messageId: 'alwaysFalsy' }, ], output: null, }, @@ -1354,7 +1471,7 @@ declare const dict: Record; if (dict['mightNotExist']) { } `, - errors: [ruleError(3, 5, 'alwaysTruthy')], + errors: [{ column: 5, line: 3, messageId: 'alwaysTruthy' }], output: null, }, { @@ -1368,8 +1485,8 @@ if (x[0]?.foo) { } `, errors: [ - ruleError(3, 5, 'alwaysTruthy'), - ruleError(5, 9, 'neverOptionalChain'), + { column: 5, line: 3, messageId: 'alwaysTruthy' }, + { column: 9, line: 5, messageId: 'neverOptionalChain' }, ], output: ` const x = [{}] as [{ foo: string }]; @@ -1386,7 +1503,7 @@ declare const arr: object[]; if (arr.filter) { } `, - errors: [ruleError(3, 5, 'alwaysTruthy')], + errors: [{ column: 5, line: 3, messageId: 'alwaysTruthy' }], output: null, }, { @@ -1400,9 +1517,9 @@ function falsy() {} [1, 2, 3].findLastIndex(falsy); `, errors: [ - ruleError(6, 18, 'alwaysTruthyFunc'), - ruleError(7, 16, 'alwaysFalsyFunc'), - ruleError(8, 25, 'alwaysFalsyFunc'), + { column: 18, line: 6, messageId: 'alwaysTruthyFunc' }, + { column: 16, line: 7, messageId: 'alwaysFalsyFunc' }, + { column: 25, line: 8, messageId: 'alwaysFalsyFunc' }, ], output: null, }, @@ -1416,7 +1533,7 @@ function falsy() {} // // Invalid: arrays are always falsy. // [[1,2], [3,4]].filter(isTruthy); // `, - // errors: [ruleError(6, 23, 'alwaysTruthyFunc')], + // errors: [({ line: 6, column: 23, messageId: 'alwaysTruthyFunc' })], // }, { code: ` @@ -1425,9 +1542,9 @@ for (; true; ) {} do {} while (true); `, errors: [ - ruleError(2, 8, 'alwaysTruthy'), - ruleError(3, 8, 'alwaysTruthy'), - ruleError(4, 14, 'alwaysTruthy'), + { column: 8, line: 2, messageId: 'alwaysTruthy' }, + { column: 8, line: 3, messageId: 'alwaysTruthy' }, + { column: 14, line: 4, messageId: 'alwaysTruthy' }, ], options: [{ allowConstantLoopConditions: false }], output: null, @@ -1868,7 +1985,7 @@ foo?.fooOrBar.baz?.qux; declare const x: { a: { b: number } }[]; x[0].a?.b; `, - errors: [ruleError(3, 7, 'neverOptionalChain')], + errors: [{ column: 7, line: 3, messageId: 'neverOptionalChain' }], output: ` declare const x: { a: { b: number } }[]; x[0].a.b; @@ -2025,7 +2142,7 @@ const a = null; if (!a) { } `, - errors: [ruleError(3, 5, 'alwaysTruthy')], + errors: [{ column: 5, line: 3, messageId: 'alwaysTruthy' }], output: null, }, { @@ -2034,7 +2151,7 @@ const a = true; if (!a) { } `, - errors: [ruleError(3, 5, 'alwaysFalsy')], + errors: [{ column: 5, line: 3, messageId: 'alwaysFalsy' }], output: null, }, { @@ -2047,7 +2164,7 @@ let speech: never = sayHi(); if (!speech) { } `, - errors: [ruleError(7, 5, 'never')], + errors: [{ column: 5, line: 7, messageId: 'never' }], output: null, }, { @@ -2400,7 +2517,7 @@ foo?.['bar']?.().toExponential(); if (!!a) { } `, - errors: [ruleError(3, 13, 'alwaysTruthy')], + errors: [{ column: 13, line: 3, messageId: 'alwaysTruthy' }], }, { code: ` From 87c3a53060dceb5f6a0d379f29c98af82e38a8c9 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Tue, 22 Oct 2024 00:45:13 -0600 Subject: [PATCH 2/7] lintfix --- .../tests/rules/no-unnecessary-condition.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 bc6b89ea8034..55a7f912de46 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -1,7 +1,4 @@ -import type { - InvalidTestCase, - TestCaseError, -} from '@typescript-eslint/rule-tester'; +import type { InvalidTestCase } from '@typescript-eslint/rule-tester'; import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; import * as path from 'node:path'; From dd15c0fdcb842c58727aa49202eaf6579fb6ec85 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Tue, 22 Oct 2024 00:57:28 -0600 Subject: [PATCH 3/7] cov --- .../rules/no-unnecessary-condition.test.ts | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) 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 55a7f912de46..1564f666d460 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -1216,6 +1216,21 @@ if (1 == '2') { }, { code: ` +// @ts-expect-error +if (1 != '2') { +} + `, + errors: [ + { + data: { trueOrFalse: 'true' }, + line: 3, + messageId: 'literalBooleanExpression', + }, + ], + output: null, + }, + { + code: ` enum Foo { a = 1, b = 2, @@ -1237,6 +1252,86 @@ if (x === Foo.a) { ], output: null, }, + { + // narrowed to null. always-true because of loose nullish equality + code: ` +function takesMaybeValue(a: null | object) { + if (a) { + } else if (a == undefined) { + } +} + `, + errors: [ + { + column: 14, + data: { trueOrFalse: 'true' }, + endColumn: 28, + endLine: 4, + line: 4, + messageId: 'literalBooleanExpression', + }, + ], + }, + { + // narrowed to null. always-false because of strict undefined equality + code: ` +function takesMaybeValue(a: null | object) { + if (a) { + } else if (a === undefined) { + } +} + `, + errors: [ + { + column: 14, + data: { trueOrFalse: 'false' }, + endColumn: 29, + endLine: 4, + line: 4, + messageId: 'literalBooleanExpression', + }, + ], + }, + { + // narrowed to null. always-false because of loose nullish equality + code: ` +function takesMaybeValue(a: null | object) { + if (a) { + } else if (a != undefined) { + } +} + `, + errors: [ + { + column: 14, + data: { trueOrFalse: 'false' }, + endColumn: 28, + endLine: 4, + line: 4, + messageId: 'literalBooleanExpression', + }, + ], + }, + { + // narrowed to null. always-true because of strict undefined equality + code: ` +function takesMaybeValue(a: null | object) { + if (a) { + } else if (a !== undefined) { + } +} + `, + errors: [ + { + column: 14, + data: { trueOrFalse: 'true' }, + endColumn: 29, + endLine: 4, + line: 4, + messageId: 'literalBooleanExpression', + }, + ], + }, // Workaround https://github.com/microsoft/TypeScript/issues/37160 { code: ` From 07a6e23f489be8908ff690885343378398012130 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Tue, 22 Oct 2024 01:30:28 -0600 Subject: [PATCH 4/7] ts-api-utils bug workaround --- .../src/rules/no-unnecessary-condition.ts | 4 ++- .../rules/no-unnecessary-condition.test.ts | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index a61296736a2c..f62a3faec42c 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -70,7 +70,9 @@ function toLiteralValue( | undefined { // type.isLiteral() only covers numbers/bigints and strings, hence the rest of the branches. if (tsutils.isBooleanLiteralType(type)) { - return { value: type.value }; + // TODO - report ts-api-utils bug. this is a workaround. for whatever reason, + // type.value is undefined. + return { value: type.intrinsicName === 'true' }; } else if (type.flags === ts.TypeFlags.Undefined) { return { value: undefined }; } else if (type.flags === ts.TypeFlags.Null) { 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 1564f666d460..c517c01d502e 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -1332,6 +1332,39 @@ function takesMaybeValue(a: null | object) { }, ], }, + { + code: ` +true === false; + `, + errors: [ + { + data: { trueOrFalse: 'false' }, + messageId: 'literalBooleanExpression', + }, + ], + }, + { + code: ` +true === true; + `, + errors: [ + { + data: { trueOrFalse: 'true' }, + messageId: 'literalBooleanExpression', + }, + ], + }, + { + code: ` +true === undefined; + `, + errors: [ + { + data: { trueOrFalse: 'false' }, + messageId: 'literalBooleanExpression', + }, + ], + }, // Workaround https://github.com/microsoft/TypeScript/issues/37160 { code: ` From 7065d0d9ceed695db84f84dc31268339d1b42342 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Tue, 22 Oct 2024 08:45:19 -0600 Subject: [PATCH 5/7] cov --- .../rules/no-unnecessary-condition.test.ts | 60 ++++--------------- 1 file changed, 12 insertions(+), 48 deletions(-) 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 c517c01d502e..2738ba0afb1f 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -1020,7 +1020,6 @@ switch (b1) { messageId: 'literalBooleanExpression', }, ], - output: null, }, // Ensure that it's complaining about the right things unnecessaryConditionTest('object', 'alwaysTruthy'), @@ -1050,7 +1049,6 @@ if (b1 || b2 || true) { { column: 11, line: 6, messageId: 'alwaysFalsy' }, { column: 17, line: 8, messageId: 'alwaysTruthy' }, ], - output: null, }, // Generic type params @@ -1061,7 +1059,6 @@ function test(t: T) { } `, errors: [{ column: 10, line: 3, messageId: 'alwaysTruthy' }], - output: null, }, { code: ` @@ -1070,7 +1067,6 @@ function test(t: T) { } `, errors: [{ column: 10, line: 3, messageId: 'alwaysFalsy' }], - output: null, }, { code: ` @@ -1079,7 +1075,6 @@ function test(t: T) { } `, errors: [{ column: 10, line: 3, messageId: 'alwaysTruthy' }], - output: null, }, // Boolean expressions @@ -1099,7 +1094,6 @@ function test(a: 'a') { messageId: 'literalBooleanExpression', }, ], - output: null, }, { code: ` @@ -1130,7 +1124,6 @@ if (y === 0) { messageId: 'literalBooleanExpression', }, ], - output: null, }, { code: ` @@ -1145,7 +1138,6 @@ if (1 == '1') { messageId: 'literalBooleanExpression', }, ], - output: null, }, { code: ` @@ -1158,7 +1150,6 @@ if (1 == '1') { messageId: 'literalBooleanExpression', }, ], - output: null, }, { code: ` @@ -1171,7 +1162,6 @@ if (1 == '1') { messageId: 'literalBooleanExpression', }, ], - output: null, }, { code: ` @@ -1184,7 +1174,6 @@ if (1 == '1') { messageId: 'literalBooleanExpression', }, ], - output: null, }, { code: ` @@ -1197,7 +1186,18 @@ if (1 == '1') { messageId: 'literalBooleanExpression', }, ], - output: null, + }, + { + code: ` +-2n !== 2n; + `, + errors: [ + { + data: { trueOrFalse: 'true' }, + line: 2, + messageId: 'literalBooleanExpression', + }, + ], }, { code: ` @@ -1212,7 +1212,6 @@ if (1 == '2') { messageId: 'literalBooleanExpression', }, ], - output: null, }, { code: ` @@ -1227,7 +1226,6 @@ if (1 != '2') { messageId: 'literalBooleanExpression', }, ], - output: null, }, { code: ` @@ -1250,7 +1248,6 @@ if (x === Foo.a) { messageId: 'literalBooleanExpression', }, ], - output: null, }, { // narrowed to null. always-true because of loose nullish equality @@ -1389,7 +1386,6 @@ function test(a: string) { { column: 14, line: 9, messageId: 'noOverlapBooleanExpression' }, { column: 14, line: 10, messageId: 'noOverlapBooleanExpression' }, ], - output: null, }, { code: ` @@ -1410,7 +1406,6 @@ function test(a?: string) { { column: 14, line: 9, messageId: 'noOverlapBooleanExpression' }, { column: 14, line: 10, messageId: 'noOverlapBooleanExpression' }, ], - output: null, }, { code: ` @@ -1431,7 +1426,6 @@ function test(a: null | string) { { column: 14, line: 5, messageId: 'noOverlapBooleanExpression' }, { column: 14, line: 6, messageId: 'noOverlapBooleanExpression' }, ], - output: null, }, { code: ` @@ -1472,7 +1466,6 @@ function test(a: T) { { column: 15, line: 17, messageId: 'noOverlapBooleanExpression' }, { column: 15, line: 18, messageId: 'noOverlapBooleanExpression' }, ], - output: null, }, // Nullish coalescing operator { @@ -1482,7 +1475,6 @@ function test(a: string) { } `, errors: [{ column: 10, line: 3, messageId: 'neverNullish' }], - output: null, }, { code: ` @@ -1491,7 +1483,6 @@ function test(a: string | false) { } `, errors: [{ column: 10, line: 3, messageId: 'neverNullish' }], - output: null, }, { code: ` @@ -1500,7 +1491,6 @@ function test(a: T) { } `, errors: [{ column: 10, line: 3, messageId: 'neverNullish' }], - output: null, }, // nullish + array index without optional chaining { @@ -1510,7 +1500,6 @@ function test(a: { foo: string }[]) { } `, errors: [{ column: 10, line: 3, messageId: 'neverNullish' }], - output: null, }, { code: ` @@ -1519,7 +1508,6 @@ function test(a: null) { } `, errors: [{ column: 10, line: 3, messageId: 'alwaysNullish' }], - output: null, }, { code: ` @@ -1528,7 +1516,6 @@ function test(a: null[]) { } `, errors: [{ column: 10, line: 3, messageId: 'alwaysNullish' }], - output: null, }, { code: ` @@ -1537,7 +1524,6 @@ function test(a: T) { } `, errors: [{ column: 10, line: 3, messageId: 'alwaysNullish' }], - output: null, }, { code: ` @@ -1546,7 +1532,6 @@ function test(a: never) { } `, errors: [{ column: 10, line: 3, messageId: 'never' }], - output: null, }, { code: ` @@ -1555,7 +1540,6 @@ function test(num: T[K]) { } `, errors: [{ column: 3, line: 3, messageId: 'neverNullish' }], - output: null, }, // Predicate functions { @@ -1585,7 +1569,6 @@ function nothing3(x: [string, string]) { { column: 25, line: 13, messageId: 'alwaysFalsy' }, { column: 25, line: 17, messageId: 'alwaysFalsy' }, ], - output: null, }, // Indexing cases { @@ -1597,7 +1580,6 @@ if (dict['mightNotExist']) { } `, errors: [{ column: 5, line: 3, messageId: 'alwaysTruthy' }], - output: null, }, { // Should still check tuples when accessed with literal numbers, since they don't have @@ -1629,7 +1611,6 @@ if (arr.filter) { } `, errors: [{ column: 5, line: 3, messageId: 'alwaysTruthy' }], - output: null, }, { code: ` @@ -1646,7 +1627,6 @@ function falsy() {} { column: 16, line: 7, messageId: 'alwaysFalsyFunc' }, { column: 25, line: 8, messageId: 'alwaysFalsyFunc' }, ], - output: null, }, // Supports generics // TODO: fix this @@ -1672,7 +1652,6 @@ do {} while (true); { column: 14, line: 4, messageId: 'alwaysTruthy' }, ], options: [{ allowConstantLoopConditions: false }], - output: null, }, { code: noFormat` @@ -2258,7 +2237,6 @@ function test(testVal?: true) { messageId: 'alwaysTruthy', }, ], - output: null, }, // https://github.com/typescript-eslint/typescript-eslint/issues/2255 { @@ -2268,7 +2246,6 @@ if (!a) { } `, errors: [{ column: 5, line: 3, messageId: 'alwaysTruthy' }], - output: null, }, { code: ` @@ -2277,7 +2254,6 @@ if (!a) { } `, errors: [{ column: 5, line: 3, messageId: 'alwaysFalsy' }], - output: null, }, { code: ` @@ -2290,7 +2266,6 @@ if (!speech) { } `, errors: [{ column: 5, line: 7, messageId: 'never' }], - output: null, }, { code: ` @@ -2315,7 +2290,6 @@ if (x) { tsconfigRootDir: path.join(rootPath, 'unstrict'), }, }, - output: null, }, { code: ` @@ -2372,7 +2346,6 @@ pick({ foo: 1, bar: 2 }, 'bar'); messageId: 'alwaysTruthy', }, ], - output: null, }, { code: ` @@ -2393,7 +2366,6 @@ function getElem(dict: Record, key: string) { messageId: 'alwaysTruthy', }, ], - output: null, }, { code: ` @@ -2409,7 +2381,6 @@ foo ??= 1; messageId: 'neverNullish', }, ], - output: null, }, { code: ` @@ -2425,7 +2396,6 @@ foo ??= 1; messageId: 'neverNullish', }, ], - output: null, }, { code: ` @@ -2441,7 +2411,6 @@ foo ??= null; messageId: 'alwaysNullish', }, ], - output: null, }, { code: ` @@ -2457,7 +2426,6 @@ foo ||= 1; messageId: 'alwaysTruthy', }, ], - output: null, }, { code: ` @@ -2473,7 +2441,6 @@ foo ||= null; messageId: 'alwaysFalsy', }, ], - output: null, }, { code: ` @@ -2489,7 +2456,6 @@ foo &&= 1; messageId: 'alwaysTruthy', }, ], - output: null, }, { code: ` @@ -2505,7 +2471,6 @@ foo &&= null; messageId: 'alwaysFalsy', }, ], - output: null, }, { code: ` @@ -2522,7 +2487,6 @@ foo.bar ??= 1; }, ], languageOptions: { parserOptions: optionsWithExactOptionalPropertyTypes }, - output: null, }, { code: ` From 0b93e529c17f8a6bc80996f0cf8d105d30fca315 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Tue, 22 Oct 2024 09:00:25 -0600 Subject: [PATCH 6/7] update todo with ts-api-utils issue --- packages/eslint-plugin/src/rules/no-unnecessary-condition.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index f62a3faec42c..35b5cf2d84b8 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -70,8 +70,9 @@ function toLiteralValue( | undefined { // type.isLiteral() only covers numbers/bigints and strings, hence the rest of the branches. if (tsutils.isBooleanLiteralType(type)) { - // TODO - report ts-api-utils bug. this is a workaround. for whatever reason, - // type.value is undefined. + // Using `type.intrinsicName` instead of `type.value` because `type.value` + // is `undefined`, contrary to what the type guard tells us. + // See https://github.com/JoshuaKGoldberg/ts-api-utils/issues/528 return { value: type.intrinsicName === 'true' }; } else if (type.flags === ts.TypeFlags.Undefined) { return { value: undefined }; From eb9ad53face5e21386a2285c0b1fcac9ebc308b9 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Mon, 4 Nov 2024 08:31:59 -0800 Subject: [PATCH 7/7] merge cleanup --- .../src/rules/no-unnecessary-condition.ts | 37 ++++++++++--------- .../rules/no-unnecessary-condition.test.ts | 6 +-- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index bf708f467777..c4bed147eb88 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -32,9 +32,11 @@ const valueIsPseudoBigInt = ( return typeof value === 'object'; }; -const getValue = (type: ts.LiteralType): bigint | number | string => { +const getValueOfLiteralType = ( + type: ts.LiteralType, +): bigint | number | string => { if (valueIsPseudoBigInt(type.value)) { - return BigInt((type.value.negative ? '-' : '') + type.value.base10Value); + return pseudoBigIntToBigInt(type.value); } return type.value; }; @@ -43,13 +45,12 @@ const isFalsyBigInt = (type: ts.Type): boolean => { return ( tsutils.isLiteralType(type) && valueIsPseudoBigInt(type.value) && - !getValue(type) + !getValueOfLiteralType(type) ); }; const isTruthyLiteral = (type: ts.Type): boolean => tsutils.isTrueLiteralType(type) || - // || type. - (type.isLiteral() && !!getValue(type)); + (type.isLiteral() && !!getValueOfLiteralType(type)); const isPossiblyFalsy = (type: ts.Type): boolean => tsutils @@ -89,7 +90,7 @@ const isPossiblyNullish = (type: ts.Type): boolean => const isAlwaysNullish = (type: ts.Type): boolean => tsutils.unionTypeParts(type).every(isNullishType); -function toLiteralValue( +function toStaticValue( type: ts.Type, ): | { value: bigint | boolean | number | string | null | undefined } @@ -100,15 +101,15 @@ function toLiteralValue( // is `undefined`, contrary to what the type guard tells us. // See https://github.com/JoshuaKGoldberg/ts-api-utils/issues/528 return { value: type.intrinsicName === 'true' }; - } else if (type.flags === ts.TypeFlags.Undefined) { + } + if (type.flags === ts.TypeFlags.Undefined) { return { value: undefined }; - } else if (type.flags === ts.TypeFlags.Null) { + } + if (type.flags === ts.TypeFlags.Null) { return { value: null }; - } else if (type.isLiteral()) { - if (typeof type.value === 'string' || typeof type.value === 'number') { - return { value: type.value }; - } - return { value: pseudoBigIntToBigInt(type.value) }; + } + if (type.isLiteral()) { + return { value: getValueOfLiteralType(type) }; } return undefined; @@ -476,14 +477,14 @@ export default createRule({ const leftType = getConstrainedTypeAtLocation(services, left); const rightType = getConstrainedTypeAtLocation(services, right); - const leftLiteralValue = toLiteralValue(leftType); - const rightLiteralValue = toLiteralValue(rightType); + const leftStaticValue = toStaticValue(leftType); + const rightStaticValue = toStaticValue(rightType); - if (leftLiteralValue != null && rightLiteralValue != null) { + if (leftStaticValue != null && rightStaticValue != null) { const conditionIsTrue = booleanComparison( - leftLiteralValue.value, + leftStaticValue.value, operator, - rightLiteralValue.value, + rightStaticValue.value, ); context.report({ 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 64f1af44e88b..7aaed5539e21 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -1043,7 +1043,7 @@ declare const falseyBigInt: 0n; if (falseyBigInt) { } `, - errors: [ruleError(3, 5, 'alwaysFalsy')], + errors: [{ column: 5, line: 3, messageId: 'alwaysFalsy' }], }, { code: ` @@ -1051,7 +1051,7 @@ declare const posbigInt: 1n; if (posbigInt) { } `, - errors: [ruleError(3, 5, 'alwaysTruthy')], + errors: [{ column: 5, line: 3, messageId: 'alwaysTruthy' }], }, { code: ` @@ -1059,7 +1059,7 @@ declare const negBigInt: -2n; if (negBigInt) { } `, - errors: [ruleError(3, 5, 'alwaysTruthy')], + errors: [{ column: 5, line: 3, messageId: 'alwaysTruthy' }], }, { code: `