From f663f696f6563d7329f0d4a58ca30e9044f15e25 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 18 Dec 2024 22:49:38 +0100 Subject: [PATCH 01/33] prefer-nullish-coalescing: handle Identifier and UnaryExpression in ternaries --- .../docs/rules/prefer-nullish-coalescing.mdx | 12 ++ .../src/rules/prefer-nullish-coalescing.ts | 68 ++++++--- .../prefer-nullish-coalescing.shot | 12 ++ .../rules/prefer-nullish-coalescing.test.ts | 144 ++++++++++++++++++ 4 files changed, 216 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx index d5742e1a8730..5df1164ae7c3 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx @@ -43,6 +43,12 @@ foo === undefined ? 'a string' : foo; const foo: string | null = 'bar'; foo !== null ? foo : 'a string'; foo === null ? 'a string' : foo; + +const foo: object | null = { value: 'bar' }; +foo !== null ? foo : { value: 'a string' }; +foo === null ? { value: 'a string' } : foo; +foo ? foo : { value: 'a string' }; +!foo ? { value: 'a string' } : foo; ``` Correct code for `ignoreTernaryTests: false`: @@ -61,6 +67,12 @@ foo ?? 'a string'; const foo: string | null = 'bar'; foo ?? 'a string'; foo ?? 'a string'; + +const foo: object | null = { value: 'bar' }; +foo ?? { value: 'a string' }; +foo ?? { value: 'a string' }; +foo ?? { value: 'a string' }; +foo ?? { value: 'a string' }; ``` ### `ignoreConditionalTests` diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index fabea709e28e..b684cca09976 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -285,7 +285,7 @@ export default createRule({ return; } - let operator: '!=' | '!==' | '==' | '===' | undefined; + let operator: '!' | '!=' | '!==' | '==' | '===' | undefined; let nodesInsideTestExpression: TSESTree.Node[] = []; if (node.test.type === AST_NODE_TYPES.BinaryExpression) { nodesInsideTestExpression = [node.test.left, node.test.right]; @@ -343,33 +343,48 @@ export default createRule({ } } - if (!operator) { - return; - } - let identifier: TSESTree.Node | undefined; let hasUndefinedCheck = false; let hasNullCheck = false; - // we check that the test only contains null, undefined and the identifier - for (const testNode of nodesInsideTestExpression) { - if (isNullLiteral(testNode)) { - hasNullCheck = true; - } else if (isUndefinedIdentifier(testNode)) { - hasUndefinedCheck = true; - } else if ( - (operator === '!==' || operator === '!=') && - isNodeEqual(testNode, node.consequent) + if (!operator) { + if ( + node.test.type === AST_NODE_TYPES.Identifier && + isNodeEqual(node.test, node.consequent) ) { - identifier = testNode; + identifier = node.test; } else if ( - (operator === '===' || operator === '==') && - isNodeEqual(testNode, node.alternate) + node.test.type === AST_NODE_TYPES.UnaryExpression && + node.test.operator === '!' && + node.test.argument.type === AST_NODE_TYPES.Identifier && + isNodeEqual(node.test.argument, node.alternate) ) { - identifier = testNode; + identifier = node.test.argument; + operator = '!'; } else { return; } + } else { + // we check that the test only contains null, undefined and the identifier + for (const testNode of nodesInsideTestExpression) { + if (isNullLiteral(testNode)) { + hasNullCheck = true; + } else if (isUndefinedIdentifier(testNode)) { + hasUndefinedCheck = true; + } else if ( + (operator === '!==' || operator === '!=') && + isNodeEqual(testNode, node.consequent) + ) { + identifier = testNode; + } else if ( + (operator === '===' || operator === '==') && + isNodeEqual(testNode, node.alternate) + ) { + identifier = testNode; + } else { + return; + } + } } if (!identifier) { @@ -378,7 +393,11 @@ export default createRule({ const isFixable = ((): boolean => { // it is fixable if we check for both null and undefined, or not if neither - if (hasUndefinedCheck === hasNullCheck) { + if ( + operator && + operator !== '!' && + hasUndefinedCheck === hasNullCheck + ) { return hasUndefinedCheck; } @@ -395,6 +414,15 @@ export default createRule({ return false; } + if (!operator || operator === '!') { + return ( + (flags & + ~(ts.TypeFlags.Null | ts.TypeFlags.Undefined) & + ts.TypeFlags.PossiblyFalsy) === + 0 + ); + } + const hasNullType = (flags & ts.TypeFlags.Null) !== 0; // it is fixable if we check for undefined and the type is not nullable @@ -420,7 +448,7 @@ export default createRule({ data: { equals: '' }, fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { const [left, right] = - operator === '===' || operator === '==' + operator === '===' || operator === '==' || operator === '!' ? [node.alternate, node.consequent] : [node.consequent, node.alternate]; return fixer.replaceText( diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot index 915762f419b3..a0160c8e7774 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot @@ -20,6 +20,12 @@ foo === undefined ? 'a string' : foo; const foo: string | null = 'bar'; foo !== null ? foo : 'a string'; foo === null ? 'a string' : foo; + +const foo: object | null = { value: 'bar' }; +foo !== null ? foo : { value: 'a string' }; +foo === null ? { value: 'a string' } : foo; +foo ? foo : { value: 'a string' }; +!foo ? { value: 'a string' } : foo; " `; @@ -39,6 +45,12 @@ foo ?? 'a string'; const foo: string | null = 'bar'; foo ?? 'a string'; foo ?? 'a string'; + +const foo: object | null = { value: 'bar' }; +foo ?? { value: 'a string' }; +foo ?? { value: 'a string' }; +foo ?? { value: 'a string' }; +foo ?? { value: 'a string' }; " `; diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 9e0bbdfab720..e992405811f1 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -125,6 +125,118 @@ x === null ? x : y; declare let x: string | null | unknown; x === null ? x : y; `, + ` +declare let x: string; +x ? x : y; + `, + ` +declare let x: string; +!x ? y : x; + `, + ` +declare let x: string | null; +x ? x : y; + `, + ` +declare let x: string | null; +!x ? y : x; + `, + ` +declare let x: string | undefined; +x ? x : y; + `, + ` +declare let x: string | undefined; +!x ? y : x; + `, + ` +declare let x: string | null | undefined; +x ? x : y; + `, + ` +declare let x: string | null | undefined; +!x ? y : x; + `, + ` +declare let x: number; +x ? x : y; + `, + ` +declare let x: number; +!x ? y : x; + `, + ` +declare let x: number | null; +x ? x : y; + `, + ` +declare let x: number | null; +!x ? y : x; + `, + ` +declare let x: number | undefined; +x ? x : y; + `, + ` +declare let x: number | undefined; +!x ? y : x; + `, + ` +declare let x: number | null | undefined; +x ? x : y; + `, + ` +declare let x: number | null | undefined; +!x ? y : x; + `, + ` +declare let x: boolean; +x ? x : y; + `, + ` +declare let x: boolean; +!x ? y : x; + `, + ` +declare let x: boolean | null; +x ? x : y; + `, + ` +declare let x: boolean | null; +!x ? y : x; + `, + ` +declare let x: boolean | undefined; +x ? x : y; + `, + ` +declare let x: boolean | undefined; +!x ? y : x; + `, + ` +declare let x: boolean | null | undefined; +x ? x : y; + `, + ` +declare let x: boolean | null | undefined; +!x ? y : x; + `, + ` +declare let x: any; +x ? x : y; + `, + ` +declare let x: any; +!x ? y : x; + `, + ` +declare let x: unknown; +x ? x : y; + `, + ` +declare let x: unknown; +!x ? y : x; + `, ].map(code => ({ code, options: [{ ignoreTernaryTests: false }] as const, @@ -787,6 +899,38 @@ x === null ? y : x; declare let x: string | null; null === x ? y : x; `, + ` +declare let x: object; +x ? x : y; + `, + ` +declare let x: object; +!x ? y : x; + `, + ` +declare let x: object | null; +x ? x : y; + `, + ` +declare let x: object | null; +!x ? y : x; + `, + ` +declare let x: object | undefined; +x ? x : y; + `, + ` +declare let x: object | undefined; +!x ? y : x; + `, + ` +declare let x: object | null | undefined; +x ? x : y; + `, + ` +declare let x: object | null | undefined; +!x ? y : x; + `, ].map(code => ({ code, errors: [ From 214e56e0e2f61da4be05ce4134fe17da59542422 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Mon, 30 Dec 2024 22:51:17 +0100 Subject: [PATCH 02/33] remove useless return --- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index b684cca09976..834e1877e988 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -381,8 +381,6 @@ export default createRule({ isNodeEqual(testNode, node.alternate) ) { identifier = testNode; - } else { - return; } } } From c0ffe540e8eee55bc3b8e0da49329bb658240803 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Mon, 30 Dec 2024 23:00:38 +0100 Subject: [PATCH 03/33] add tests --- .../rules/prefer-nullish-coalescing.test.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index e992405811f1..4def5fb1e9ca 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -2473,5 +2473,67 @@ if (+(a ?? b)) { }, ], }, + { + code: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBox: Box | undefined; + +defaultBox || getFallbackBox(); + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBox: Box | undefined; + +defaultBox ?? getFallbackBox(); + `, + }, + ], + }, + ], + }, + { + code: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBox: Box | undefined; + +defaultBox ? defaultBox : getFallbackBox(); + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +interface Box { + value: string; +} +declare function getFallbackBox(): Box; +declare const defaultBox: Box | undefined; + +defaultBox ?? getFallbackBox(); + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, ], }); From 08dcde6212bbcfdf8d2aaf883691fa1ca1b45721 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 31 Dec 2024 01:07:30 +0100 Subject: [PATCH 04/33] create intermediate constants for clarity --- .../src/rules/prefer-nullish-coalescing.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 834e1877e988..400021474006 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -390,12 +390,10 @@ export default createRule({ } const isFixable = ((): boolean => { + const implicitEquality = !operator || operator === '!'; + // it is fixable if we check for both null and undefined, or not if neither - if ( - operator && - operator !== '!' && - hasUndefinedCheck === hasNullCheck - ) { + if (!implicitEquality && hasUndefinedCheck === hasNullCheck) { return hasUndefinedCheck; } @@ -412,13 +410,10 @@ export default createRule({ return false; } - if (!operator || operator === '!') { - return ( - (flags & - ~(ts.TypeFlags.Null | ts.TypeFlags.Undefined) & - ts.TypeFlags.PossiblyFalsy) === - 0 - ); + const nullishFlags = ts.TypeFlags.Null | ts.TypeFlags.Undefined; + + if (implicitEquality) { + return (flags & ~nullishFlags & ts.TypeFlags.PossiblyFalsy) === 0; } const hasNullType = (flags & ts.TypeFlags.Null) !== 0; From bde98e913225bb19aa458be3c209ea8f160da2fd Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 31 Dec 2024 09:30:36 +0100 Subject: [PATCH 05/33] move line --- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 400021474006..61a58b8a5e62 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -410,9 +410,8 @@ export default createRule({ return false; } - const nullishFlags = ts.TypeFlags.Null | ts.TypeFlags.Undefined; - if (implicitEquality) { + const nullishFlags = ts.TypeFlags.Null | ts.TypeFlags.Undefined; return (flags & ~nullishFlags & ts.TypeFlags.PossiblyFalsy) === 0; } From d6fa5594dd0e1603416b8a1aecb0581a89cab00a Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:20:43 +0100 Subject: [PATCH 06/33] add tests --- .../rules/prefer-nullish-coalescing.test.ts | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 4def5fb1e9ca..e74b52418ea8 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -187,6 +187,38 @@ x ? x : y; `, ` declare let x: number | null | undefined; +!x ? y : x; + `, + ` +declare let x: bigint; +x ? x : y; + `, + ` +declare let x: bigint; +!x ? y : x; + `, + ` +declare let x: bigint | null; +x ? x : y; + `, + ` +declare let x: bigint | null; +!x ? y : x; + `, + ` +declare let x: bigint | undefined; +x ? x : y; + `, + ` +declare let x: bigint | undefined; +!x ? y : x; + `, + ` +declare let x: bigint | null | undefined; +x ? x : y; + `, + ` +declare let x: bigint | null | undefined; !x ? y : x; `, ` @@ -929,6 +961,94 @@ x ? x : y; `, ` declare let x: object | null | undefined; +!x ? y : x; + `, + ` +declare let x: Function; +x ? x : y; + `, + ` +declare let x: Function; +!x ? y : x; + `, + ` +declare let x: Function | null; +x ? x : y; + `, + ` +declare let x: Function | null; +!x ? y : x; + `, + ` +declare let x: Function | undefined; +x ? x : y; + `, + ` +declare let x: Function | undefined; +!x ? y : x; + `, + ` +declare let x: Function | null | undefined; +x ? x : y; + `, + ` +declare let x: Function | null | undefined; +!x ? y : x; + `, + ` +declare let x: () => string; +x ? x : y; + `, + ` +declare let x: () => string; +!x ? y : x; + `, + ` +declare let x: () => string | null; +x ? x : y; + `, + ` +declare let x: () => string | null; +!x ? y : x; + `, + ` +declare let x: () => string | undefined; +x ? x : y; + `, + ` +declare let x: () => string | undefined; +!x ? y : x; + `, + ` +declare let x: () => string | null | undefined; +x ? x : y; + `, + ` +declare let x: () => string | null | undefined; +!x ? y : x; + `, + ` +declare let x: (() => string) | null; +x ? x : y; + `, + ` +declare let x: (() => string) | null; +!x ? y : x; + `, + ` +declare let x: (() => string) | undefined; +x ? x : y; + `, + ` +declare let x: (() => string) | undefined; +!x ? y : x; + `, + ` +declare let x: (() => string) | null | undefined; +x ? x : y; + `, + ` +declare let x: (() => string) | null | undefined; !x ? y : x; `, ].map(code => ({ From ec6c08f087c68d4e00d045ec8298572755acf46f Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:25:54 +0100 Subject: [PATCH 07/33] add more tests --- .../rules/prefer-nullish-coalescing.test.ts | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index e74b52418ea8..f1b6dd73fb70 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -155,6 +155,38 @@ x ? x : y; `, ` declare let x: string | null | undefined; +!x ? y : x; + `, + ` +declare let x: string | object; +x ? x : y; + `, + ` +declare let x: string | object; +!x ? y : x; + `, + ` +declare let x: string | object | null; +x ? x : y; + `, + ` +declare let x: string | object | null; +!x ? y : x; + `, + ` +declare let x: string | object | undefined; +x ? x : y; + `, + ` +declare let x: string | object | undefined; +!x ? y : x; + `, + ` +declare let x: string | object | null | undefined; +x ? x : y; + `, + ` +declare let x: string | object | null | undefined; !x ? y : x; `, ` @@ -259,6 +291,30 @@ x ? x : y; `, ` declare let x: any; +!x ? y : x; + `, + ` +declare let x: any | null; +x ? x : y; + `, + ` +declare let x: any | null; +!x ? y : x; + `, + ` +declare let x: any | undefined; +x ? x : y; + `, + ` +declare let x: any | undefined; +!x ? y : x; + `, + ` +declare let x: any | null | undefined; +x ? x : y; + `, + ` +declare let x: any | null | undefined; !x ? y : x; `, ` @@ -267,6 +323,254 @@ x ? x : y; `, ` declare let x: unknown; +!x ? y : x; + `, + ` +declare let x: unknown | null; +x ? x : y; + `, + ` +declare let x: unknown | null; +!x ? y : x; + `, + ` +declare let x: unknown | undefined; +x ? x : y; + `, + ` +declare let x: unknown | undefined; +!x ? y : x; + `, + ` +declare let x: unknown | null | undefined; +x ? x : y; + `, + ` +declare let x: unknown | null | undefined; +!x ? y : x; + `, + ` +declare let x: any | unknown; +x ? x : y; + `, + ` +declare let x: any | unknown; +!x ? y : x; + `, + ` +declare let x: any | unknown | null; +x ? x : y; + `, + ` +declare let x: any | unknown | null; +!x ? y : x; + `, + ` +declare let x: any | unknown | undefined; +x ? x : y; + `, + ` +declare let x: any | unknown | undefined; +!x ? y : x; + `, + ` +declare let x: any | unknown | null | undefined; +x ? x : y; + `, + ` +declare let x: any | unknown | null | undefined; +!x ? y : x; + `, + ` +declare let x: string | any; +x ? x : y; + `, + ` +declare let x: string | any; +!x ? y : x; + `, + ` +declare let x: string | any | null; +x ? x : y; + `, + ` +declare let x: string | any | null; +!x ? y : x; + `, + ` +declare let x: string | any | undefined; +x ? x : y; + `, + ` +declare let x: string | any | undefined; +!x ? y : x; + `, + ` +declare let x: string | any | null | undefined; +x ? x : y; + `, + ` +declare let x: string | any | null | undefined; +!x ? y : x; + `, + ` +declare let x: string | unknown; +x ? x : y; + `, + ` +declare let x: string | unknown; +!x ? y : x; + `, + ` +declare let x: string | unknown | null; +x ? x : y; + `, + ` +declare let x: string | unknown | null; +!x ? y : x; + `, + ` +declare let x: string | unknown | undefined; +x ? x : y; + `, + ` +declare let x: string | unknown | undefined; +!x ? y : x; + `, + ` +declare let x: string | unknown | null | undefined; +x ? x : y; + `, + ` +declare let x: string | unknown | null | undefined; +!x ? y : x; + `, + ` +declare let x: string | any | unknown; +x ? x : y; + `, + ` +declare let x: string | any | unknown; +!x ? y : x; + `, + ` +declare let x: string | any | unknown | null; +x ? x : y; + `, + ` +declare let x: string | any | unknown | null; +!x ? y : x; + `, + ` +declare let x: string | any | unknown | undefined; +x ? x : y; + `, + ` +declare let x: string | any | unknown | undefined; +!x ? y : x; + `, + ` +declare let x: string | any | unknown | null | undefined; +x ? x : y; + `, + ` +declare let x: string | any | unknown | null | undefined; +!x ? y : x; + `, + ` +declare let x: object | any; +x ? x : y; + `, + ` +declare let x: object | any; +!x ? y : x; + `, + ` +declare let x: object | any | null; +x ? x : y; + `, + ` +declare let x: object | any | null; +!x ? y : x; + `, + ` +declare let x: object | any | undefined; +x ? x : y; + `, + ` +declare let x: object | any | undefined; +!x ? y : x; + `, + ` +declare let x: object | any | null | undefined; +x ? x : y; + `, + ` +declare let x: object | any | null | undefined; +!x ? y : x; + `, + ` +declare let x: object | unknown; +x ? x : y; + `, + ` +declare let x: object | unknown; +!x ? y : x; + `, + ` +declare let x: object | unknown | null; +x ? x : y; + `, + ` +declare let x: object | unknown | null; +!x ? y : x; + `, + ` +declare let x: object | unknown | undefined; +x ? x : y; + `, + ` +declare let x: object | unknown | undefined; +!x ? y : x; + `, + ` +declare let x: object | unknown | null | undefined; +x ? x : y; + `, + ` +declare let x: object | unknown | null | undefined; +!x ? y : x; + `, + ` +declare let x: object | any | unknown; +x ? x : y; + `, + ` +declare let x: object | any | unknown; +!x ? y : x; + `, + ` +declare let x: object | any | unknown | null; +x ? x : y; + `, + ` +declare let x: object | any | unknown | null; +!x ? y : x; + `, + ` +declare let x: object | any | unknown | undefined; +x ? x : y; + `, + ` +declare let x: object | any | unknown | undefined; +!x ? y : x; + `, + ` +declare let x: object | any | unknown | null | undefined; +x ? x : y; + `, + ` +declare let x: object | any | unknown | null | undefined; !x ? y : x; `, ].map(code => ({ From 5734d33390cea0982f16a3bc77e21617d55c3d2b Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:17:34 +0100 Subject: [PATCH 08/33] add ouput --- .../eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index f1b6dd73fb70..e45130021ad3 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -2926,6 +2926,7 @@ defaultBox ?? getFallbackBox(); ], }, ], + output: null, }, { code: ` From 7ff3d6d6e336b04766ee9203f3518fc44dba0011 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sat, 4 Jan 2025 01:16:35 +0100 Subject: [PATCH 09/33] renaming + migrate some utils from rules to util folder --- .../src/rules/no-unnecessary-condition.ts | 62 ++----------------- .../src/rules/prefer-nullish-coalescing.ts | 15 +++-- .../src/util/getValueOfLiteralType.ts | 20 ++++++ packages/eslint-plugin/src/util/index.ts | 2 + .../src/util/truthinessAndNullishUtils.ts | 41 ++++++++++++ 5 files changed, 77 insertions(+), 63 deletions(-) create mode 100644 packages/eslint-plugin/src/util/getValueOfLiteralType.ts create mode 100644 packages/eslint-plugin/src/util/truthinessAndNullishUtils.ts diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 97664597ef6d..b8b77ef2d530 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -11,9 +11,14 @@ import { getParserServices, getTypeName, getTypeOfPropertyOfName, + getValueOfLiteralType, + isAlwaysNullish, isArrayMethodCallWithPredicate, isIdentifier, isNullableType, + isPossiblyFalsy, + isPossiblyNullish, + isPossiblyTruthy, isTypeAnyType, isTypeFlagSet, isTypeUnknownType, @@ -25,59 +30,7 @@ import { findTypeGuardAssertedArgument, } from '../util/assertionFunctionUtils'; -// Truthiness utilities // #region -const valueIsPseudoBigInt = ( - value: number | string | ts.PseudoBigInt, -): value is ts.PseudoBigInt => { - return typeof value === 'object'; -}; - -const getValueOfLiteralType = ( - type: ts.LiteralType, -): bigint | number | string => { - if (valueIsPseudoBigInt(type.value)) { - return pseudoBigIntToBigInt(type.value); - } - return type.value; -}; - -const isTruthyLiteral = (type: ts.Type): boolean => - tsutils.isTrueLiteralType(type) || - (type.isLiteral() && !!getValueOfLiteralType(type)); - -const isPossiblyFalsy = (type: ts.Type): boolean => - tsutils - .unionTypeParts(type) - // Intersections like `string & {}` can also be possibly falsy, - // requiring us to look into the intersection. - .flatMap(type => tsutils.intersectionTypeParts(type)) - // PossiblyFalsy flag includes literal values, so exclude ones that - // are definitely truthy - .filter(t => !isTruthyLiteral(t)) - .some(type => isTypeFlagSet(type, ts.TypeFlags.PossiblyFalsy)); - -const isPossiblyTruthy = (type: ts.Type): boolean => - tsutils - .unionTypeParts(type) - .map(type => tsutils.intersectionTypeParts(type)) - .some(intersectionParts => - // It is possible to define intersections that are always falsy, - // like `"" & { __brand: string }`. - intersectionParts.every(type => !tsutils.isFalsyType(type)), - ); - -// Nullish utilities -const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null; -const isNullishType = (type: ts.Type): boolean => - isTypeFlagSet(type, nullishFlag); - -const isPossiblyNullish = (type: ts.Type): boolean => - tsutils.unionTypeParts(type).some(isNullishType); - -const isAlwaysNullish = (type: ts.Type): boolean => - tsutils.unionTypeParts(type).every(isNullishType); - function toStaticValue( type: ts.Type, ): @@ -100,10 +53,6 @@ function toStaticValue( return undefined; } -function pseudoBigIntToBigInt(value: ts.PseudoBigInt): bigint { - return BigInt((value.negative ? '-' : '') + value.base10Value); -} - const BOOL_OPERATORS = new Set([ '<', '>', @@ -151,7 +100,6 @@ function booleanComparison( return left >= right; } } - // #endregion export type Options = [ diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 61a58b8a5e62..dda2032a8b45 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -12,6 +12,7 @@ import { isLogicalOrOperator, isNodeEqual, isNullLiteral, + isPossiblyFalsy, isTypeFlagSet, isUndefinedIdentifier, nullThrows, @@ -346,8 +347,12 @@ export default createRule({ let identifier: TSESTree.Node | undefined; let hasUndefinedCheck = false; let hasNullCheck = false; + let hasTruthinessCheck = false; if (!operator) { + hasUndefinedCheck = true; + hasNullCheck = true; + hasTruthinessCheck = true; if ( node.test.type === AST_NODE_TYPES.Identifier && isNodeEqual(node.test, node.consequent) @@ -390,10 +395,8 @@ export default createRule({ } const isFixable = ((): boolean => { - const implicitEquality = !operator || operator === '!'; - // it is fixable if we check for both null and undefined, or not if neither - if (!implicitEquality && hasUndefinedCheck === hasNullCheck) { + if (!hasTruthinessCheck && hasUndefinedCheck === hasNullCheck) { return hasUndefinedCheck; } @@ -410,9 +413,9 @@ export default createRule({ return false; } - if (implicitEquality) { - const nullishFlags = ts.TypeFlags.Null | ts.TypeFlags.Undefined; - return (flags & ~nullishFlags & ts.TypeFlags.PossiblyFalsy) === 0; + if (hasTruthinessCheck) { + const nonNullishType = checker.getNonNullableType(type); + return !isPossiblyFalsy(nonNullishType); } const hasNullType = (flags & ts.TypeFlags.Null) !== 0; diff --git a/packages/eslint-plugin/src/util/getValueOfLiteralType.ts b/packages/eslint-plugin/src/util/getValueOfLiteralType.ts new file mode 100644 index 000000000000..78f61407ffe7 --- /dev/null +++ b/packages/eslint-plugin/src/util/getValueOfLiteralType.ts @@ -0,0 +1,20 @@ +import type * as ts from 'typescript'; + +const valueIsPseudoBigInt = ( + value: number | string | ts.PseudoBigInt, +): value is ts.PseudoBigInt => { + return typeof value === 'object'; +}; + +const pseudoBigIntToBigInt = (value: ts.PseudoBigInt): bigint => { + return BigInt((value.negative ? '-' : '') + value.base10Value); +}; + +export const getValueOfLiteralType = ( + type: ts.LiteralType, +): bigint | number | string => { + if (valueIsPseudoBigInt(type.value)) { + return pseudoBigIntToBigInt(type.value); + } + return type.value; +}; diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 934956e91ad0..5486991b65d3 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -24,6 +24,8 @@ export * from './needsToBeAwaited'; export * from './scopeUtils'; export * from './types'; export * from './getConstraintInfo'; +export * from './getValueOfLiteralType'; +export * from './truthinessAndNullishUtils'; // this is done for convenience - saves migrating all of the old rules export * from '@typescript-eslint/type-utils'; diff --git a/packages/eslint-plugin/src/util/truthinessAndNullishUtils.ts b/packages/eslint-plugin/src/util/truthinessAndNullishUtils.ts new file mode 100644 index 000000000000..b35e0334719d --- /dev/null +++ b/packages/eslint-plugin/src/util/truthinessAndNullishUtils.ts @@ -0,0 +1,41 @@ +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; + +import { getValueOfLiteralType } from './getValueOfLiteralType'; + +// Truthiness utilities +const isTruthyLiteral = (type: ts.Type): boolean => + tsutils.isTrueLiteralType(type) || + (type.isLiteral() && !!getValueOfLiteralType(type)); + +export const isPossiblyFalsy = (type: ts.Type): boolean => + tsutils + .unionTypeParts(type) + // Intersections like `string & {}` can also be possibly falsy, + // requiring us to look into the intersection. + .flatMap(type => tsutils.intersectionTypeParts(type)) + // PossiblyFalsy flag includes literal values, so exclude ones that + // are definitely truthy + .filter(t => !isTruthyLiteral(t)) + .some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.PossiblyFalsy)); + +export const isPossiblyTruthy = (type: ts.Type): boolean => + tsutils + .unionTypeParts(type) + .map(type => tsutils.intersectionTypeParts(type)) + .some(intersectionParts => + // It is possible to define intersections that are always falsy, + // like `"" & { __brand: string }`. + intersectionParts.every(type => !tsutils.isFalsyType(type)), + ); + +// Nullish utilities +const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null; +const isNullishType = (type: ts.Type): boolean => + tsutils.isTypeFlagSet(type, nullishFlag); + +export const isPossiblyNullish = (type: ts.Type): boolean => + tsutils.unionTypeParts(type).some(isNullishType); + +export const isAlwaysNullish = (type: ts.Type): boolean => + tsutils.unionTypeParts(type).every(isNullishType); From 0274a0cf0f6a168a75c28933df325f4ee0bacc81 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sat, 4 Jan 2025 10:51:44 +0100 Subject: [PATCH 10/33] remove redundant tests --- .../rules/prefer-nullish-coalescing.test.ts | 272 ------------------ 1 file changed, 272 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index e45130021ad3..b548b36d69da 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -291,30 +291,6 @@ x ? x : y; `, ` declare let x: any; -!x ? y : x; - `, - ` -declare let x: any | null; -x ? x : y; - `, - ` -declare let x: any | null; -!x ? y : x; - `, - ` -declare let x: any | undefined; -x ? x : y; - `, - ` -declare let x: any | undefined; -!x ? y : x; - `, - ` -declare let x: any | null | undefined; -x ? x : y; - `, - ` -declare let x: any | null | undefined; !x ? y : x; `, ` @@ -323,254 +299,6 @@ x ? x : y; `, ` declare let x: unknown; -!x ? y : x; - `, - ` -declare let x: unknown | null; -x ? x : y; - `, - ` -declare let x: unknown | null; -!x ? y : x; - `, - ` -declare let x: unknown | undefined; -x ? x : y; - `, - ` -declare let x: unknown | undefined; -!x ? y : x; - `, - ` -declare let x: unknown | null | undefined; -x ? x : y; - `, - ` -declare let x: unknown | null | undefined; -!x ? y : x; - `, - ` -declare let x: any | unknown; -x ? x : y; - `, - ` -declare let x: any | unknown; -!x ? y : x; - `, - ` -declare let x: any | unknown | null; -x ? x : y; - `, - ` -declare let x: any | unknown | null; -!x ? y : x; - `, - ` -declare let x: any | unknown | undefined; -x ? x : y; - `, - ` -declare let x: any | unknown | undefined; -!x ? y : x; - `, - ` -declare let x: any | unknown | null | undefined; -x ? x : y; - `, - ` -declare let x: any | unknown | null | undefined; -!x ? y : x; - `, - ` -declare let x: string | any; -x ? x : y; - `, - ` -declare let x: string | any; -!x ? y : x; - `, - ` -declare let x: string | any | null; -x ? x : y; - `, - ` -declare let x: string | any | null; -!x ? y : x; - `, - ` -declare let x: string | any | undefined; -x ? x : y; - `, - ` -declare let x: string | any | undefined; -!x ? y : x; - `, - ` -declare let x: string | any | null | undefined; -x ? x : y; - `, - ` -declare let x: string | any | null | undefined; -!x ? y : x; - `, - ` -declare let x: string | unknown; -x ? x : y; - `, - ` -declare let x: string | unknown; -!x ? y : x; - `, - ` -declare let x: string | unknown | null; -x ? x : y; - `, - ` -declare let x: string | unknown | null; -!x ? y : x; - `, - ` -declare let x: string | unknown | undefined; -x ? x : y; - `, - ` -declare let x: string | unknown | undefined; -!x ? y : x; - `, - ` -declare let x: string | unknown | null | undefined; -x ? x : y; - `, - ` -declare let x: string | unknown | null | undefined; -!x ? y : x; - `, - ` -declare let x: string | any | unknown; -x ? x : y; - `, - ` -declare let x: string | any | unknown; -!x ? y : x; - `, - ` -declare let x: string | any | unknown | null; -x ? x : y; - `, - ` -declare let x: string | any | unknown | null; -!x ? y : x; - `, - ` -declare let x: string | any | unknown | undefined; -x ? x : y; - `, - ` -declare let x: string | any | unknown | undefined; -!x ? y : x; - `, - ` -declare let x: string | any | unknown | null | undefined; -x ? x : y; - `, - ` -declare let x: string | any | unknown | null | undefined; -!x ? y : x; - `, - ` -declare let x: object | any; -x ? x : y; - `, - ` -declare let x: object | any; -!x ? y : x; - `, - ` -declare let x: object | any | null; -x ? x : y; - `, - ` -declare let x: object | any | null; -!x ? y : x; - `, - ` -declare let x: object | any | undefined; -x ? x : y; - `, - ` -declare let x: object | any | undefined; -!x ? y : x; - `, - ` -declare let x: object | any | null | undefined; -x ? x : y; - `, - ` -declare let x: object | any | null | undefined; -!x ? y : x; - `, - ` -declare let x: object | unknown; -x ? x : y; - `, - ` -declare let x: object | unknown; -!x ? y : x; - `, - ` -declare let x: object | unknown | null; -x ? x : y; - `, - ` -declare let x: object | unknown | null; -!x ? y : x; - `, - ` -declare let x: object | unknown | undefined; -x ? x : y; - `, - ` -declare let x: object | unknown | undefined; -!x ? y : x; - `, - ` -declare let x: object | unknown | null | undefined; -x ? x : y; - `, - ` -declare let x: object | unknown | null | undefined; -!x ? y : x; - `, - ` -declare let x: object | any | unknown; -x ? x : y; - `, - ` -declare let x: object | any | unknown; -!x ? y : x; - `, - ` -declare let x: object | any | unknown | null; -x ? x : y; - `, - ` -declare let x: object | any | unknown | null; -!x ? y : x; - `, - ` -declare let x: object | any | unknown | undefined; -x ? x : y; - `, - ` -declare let x: object | any | unknown | undefined; -!x ? y : x; - `, - ` -declare let x: object | any | unknown | null | undefined; -x ? x : y; - `, - ` -declare let x: object | any | unknown | null | undefined; !x ? y : x; `, ].map(code => ({ From d92734e20fbbf070173e864df49e5138333979d3 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sat, 4 Jan 2025 11:29:36 +0100 Subject: [PATCH 11/33] remove useless return --- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index dda2032a8b45..8d2e5cc97419 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -366,8 +366,6 @@ export default createRule({ ) { identifier = node.test.argument; operator = '!'; - } else { - return; } } else { // we check that the test only contains null, undefined and the identifier From 7a85531494c0bc6323cb6cc37b62d6da84aedbe5 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sat, 4 Jan 2025 12:22:50 +0100 Subject: [PATCH 12/33] handle MemberExpression --- .../src/rules/prefer-nullish-coalescing.ts | 8 ++- .../rules/prefer-nullish-coalescing.test.ts | 56 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 8d2e5cc97419..aac34ba37794 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -19,6 +19,9 @@ import { NullThrowsReasons, } from '../util'; +const isIdentifierOrMemberExpression = (type: TSESTree.AST_NODE_TYPES) => + [AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression].includes(type); + export type Options = [ { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; @@ -353,15 +356,16 @@ export default createRule({ hasUndefinedCheck = true; hasNullCheck = true; hasTruthinessCheck = true; + if ( - node.test.type === AST_NODE_TYPES.Identifier && + isIdentifierOrMemberExpression(node.test.type) && isNodeEqual(node.test, node.consequent) ) { identifier = node.test; } else if ( node.test.type === AST_NODE_TYPES.UnaryExpression && node.test.operator === '!' && - node.test.argument.type === AST_NODE_TYPES.Identifier && + isIdentifierOrMemberExpression(node.test.argument.type) && isNodeEqual(node.test.argument, node.alternate) ) { identifier = node.test.argument; diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index b548b36d69da..1ce69466484c 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -301,6 +301,10 @@ x ? x : y; declare let x: unknown; !x ? y : x; `, + ` +declare let x: { n: string }; +x.n ? x.n : y; + `, ].map(code => ({ code, options: [{ ignoreTernaryTests: false }] as const, @@ -2627,6 +2631,58 @@ if (+(a ?? b)) { }, { code: ` +declare const x: { n: object }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: object }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: object }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: object }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` interface Box { value: string; } From eca719691becf2deea21279904b7b6cf4a9d72b7 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sat, 4 Jan 2025 22:29:23 +0100 Subject: [PATCH 13/33] keep no-unnecessary-condition utils update for another PR --- .../src/rules/no-unnecessary-condition.ts | 62 +++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index b8b77ef2d530..97664597ef6d 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -11,14 +11,9 @@ import { getParserServices, getTypeName, getTypeOfPropertyOfName, - getValueOfLiteralType, - isAlwaysNullish, isArrayMethodCallWithPredicate, isIdentifier, isNullableType, - isPossiblyFalsy, - isPossiblyNullish, - isPossiblyTruthy, isTypeAnyType, isTypeFlagSet, isTypeUnknownType, @@ -30,7 +25,59 @@ import { findTypeGuardAssertedArgument, } from '../util/assertionFunctionUtils'; +// Truthiness utilities // #region +const valueIsPseudoBigInt = ( + value: number | string | ts.PseudoBigInt, +): value is ts.PseudoBigInt => { + return typeof value === 'object'; +}; + +const getValueOfLiteralType = ( + type: ts.LiteralType, +): bigint | number | string => { + if (valueIsPseudoBigInt(type.value)) { + return pseudoBigIntToBigInt(type.value); + } + return type.value; +}; + +const isTruthyLiteral = (type: ts.Type): boolean => + tsutils.isTrueLiteralType(type) || + (type.isLiteral() && !!getValueOfLiteralType(type)); + +const isPossiblyFalsy = (type: ts.Type): boolean => + tsutils + .unionTypeParts(type) + // Intersections like `string & {}` can also be possibly falsy, + // requiring us to look into the intersection. + .flatMap(type => tsutils.intersectionTypeParts(type)) + // PossiblyFalsy flag includes literal values, so exclude ones that + // are definitely truthy + .filter(t => !isTruthyLiteral(t)) + .some(type => isTypeFlagSet(type, ts.TypeFlags.PossiblyFalsy)); + +const isPossiblyTruthy = (type: ts.Type): boolean => + tsutils + .unionTypeParts(type) + .map(type => tsutils.intersectionTypeParts(type)) + .some(intersectionParts => + // It is possible to define intersections that are always falsy, + // like `"" & { __brand: string }`. + intersectionParts.every(type => !tsutils.isFalsyType(type)), + ); + +// Nullish utilities +const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null; +const isNullishType = (type: ts.Type): boolean => + isTypeFlagSet(type, nullishFlag); + +const isPossiblyNullish = (type: ts.Type): boolean => + tsutils.unionTypeParts(type).some(isNullishType); + +const isAlwaysNullish = (type: ts.Type): boolean => + tsutils.unionTypeParts(type).every(isNullishType); + function toStaticValue( type: ts.Type, ): @@ -53,6 +100,10 @@ function toStaticValue( return undefined; } +function pseudoBigIntToBigInt(value: ts.PseudoBigInt): bigint { + return BigInt((value.negative ? '-' : '') + value.base10Value); +} + const BOOL_OPERATORS = new Set([ '<', '>', @@ -100,6 +151,7 @@ function booleanComparison( return left >= right; } } + // #endregion export type Options = [ From c8d56b3b3ded944f35c41727e9e54cbfa6c49bff Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sat, 4 Jan 2025 23:14:49 +0100 Subject: [PATCH 14/33] add tests --- .../rules/prefer-nullish-coalescing.test.ts | 1140 +++++++++++++++++ 1 file changed, 1140 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 1ce69466484c..2a93130da77d 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -305,6 +305,178 @@ declare let x: unknown; declare let x: { n: string }; x.n ? x.n : y; `, + ` +declare let x: { n: string }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | object }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | object }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | object | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | object | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | object | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | object | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: string | object | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: string | object | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: number }; +x.n ? x.n : y; + `, + ` +declare let x: { n: number }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: number | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: number | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: number | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: number | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: number | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: number | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: bigint }; +x.n ? x.n : y; + `, + ` +declare let x: { n: bigint }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: bigint | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: bigint | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: bigint | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: bigint | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: bigint | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: bigint | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: boolean }; +x.n ? x.n : y; + `, + ` +declare let x: { n: boolean }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: boolean | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: boolean | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: boolean | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: boolean | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: boolean | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: boolean | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: any }; +x.n ? x.n : y; + `, + ` +declare let x: { n: any }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: unknown }; +x.n ? x.n : y; + `, + ` +declare let x: { n: unknown }; +!x.n ? y : x.n; + `, ].map(code => ({ code, options: [{ ignoreTernaryTests: false }] as const, @@ -968,6 +1140,38 @@ declare let x: string | null; null === x ? y : x; `, ` +declare let x: string[]; +x ? x : y; + `, + ` +declare let x: string[]; +!x ? y : x; + `, + ` +declare let x: string[] | null; +x ? x : y; + `, + ` +declare let x: string[] | null; +!x ? y : x; + `, + ` +declare let x: string[] | undefined; +x ? x : y; + `, + ` +declare let x: string[] | undefined; +!x ? y : x; + `, + ` +declare let x: string[] | null | undefined; +x ? x : y; + `, + ` +declare let x: string[] | null | undefined; +!x ? y : x; + `, + ` declare let x: object; x ? x : y; `, @@ -2672,6 +2876,942 @@ declare const y: any; declare const x: { n: object }; declare const y: any; +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: object | null }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: object | null }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: object | null }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: object | null }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: object | undefined }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: object | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: object | undefined }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: object | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: object | null | undefined }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: object | null | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: object | null | undefined }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: object | null | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: string[] }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: string[] }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: string[] }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: string[] }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: string[] | null }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: string[] | null }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: string[] | null }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: string[] | null }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: string[] | undefined }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: string[] | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: string[] | undefined }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: string[] | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: string[] | null | undefined }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: string[] | null | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: string[] | null | undefined }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: string[] | null | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: Function }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: Function }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: Function }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: Function }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: Function | null }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: Function | null }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: Function | null }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: Function | null }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: Function | undefined }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: Function | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: Function | undefined }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: Function | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: Function | null | undefined }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: Function | null | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: Function | null | undefined }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: Function | null | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: () => string }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: () => string }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: () => string }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: () => string }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: () => string | null }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: () => string | null }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: () => string | null }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: () => string | null }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: () => string | undefined }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: () => string | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: () => string | undefined }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: () => string | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: () => string | null | undefined }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: () => string | null | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: () => string | null | undefined }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: () => string | null | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: (() => string) | null }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: (() => string) | null }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: (() => string) | null }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: (() => string) | null }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: (() => string) | undefined }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: (() => string) | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: (() => string) | undefined }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: (() => string) | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: (() => string) | null | undefined }; +declare const y: any; + +x.n ? x.n : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: (() => string) | null | undefined }; +declare const y: any; + +x.n ?? y; + `, + }, + ], + }, + ], + options: [{ ignoreTernaryTests: false }], + output: null, + }, + { + code: ` +declare const x: { n: (() => string) | null | undefined }; +declare const y: any; + +!x.n ? y : x.n; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: { n: (() => string) | null | undefined }; +declare const y: any; + x.n ?? y; `, }, From 0494327610c99bc2f4aa000a6ab70259674c3bd3 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 5 Jan 2025 00:00:48 +0100 Subject: [PATCH 15/33] renaming --- .../eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index aac34ba37794..8554018de81e 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -19,7 +19,7 @@ import { NullThrowsReasons, } from '../util'; -const isIdentifierOrMemberExpression = (type: TSESTree.AST_NODE_TYPES) => +const isIdentifierOrMemberExpressionType = (type: TSESTree.AST_NODE_TYPES) => [AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression].includes(type); export type Options = [ @@ -358,14 +358,14 @@ export default createRule({ hasTruthinessCheck = true; if ( - isIdentifierOrMemberExpression(node.test.type) && + isIdentifierOrMemberExpressionType(node.test.type) && isNodeEqual(node.test, node.consequent) ) { identifier = node.test; } else if ( node.test.type === AST_NODE_TYPES.UnaryExpression && node.test.operator === '!' && - isIdentifierOrMemberExpression(node.test.argument.type) && + isIdentifierOrMemberExpressionType(node.test.argument.type) && isNodeEqual(node.test.argument, node.alternate) ) { identifier = node.test.argument; From ab5d7d85811088d8d26c44642ecab008ed9a3fa0 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 5 Jan 2025 00:57:09 +0100 Subject: [PATCH 16/33] add tests for new utils --- .../tests/util/getValueOfLiteralType.test.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 packages/eslint-plugin/tests/util/getValueOfLiteralType.test.ts diff --git a/packages/eslint-plugin/tests/util/getValueOfLiteralType.test.ts b/packages/eslint-plugin/tests/util/getValueOfLiteralType.test.ts new file mode 100644 index 000000000000..35a79242dc79 --- /dev/null +++ b/packages/eslint-plugin/tests/util/getValueOfLiteralType.test.ts @@ -0,0 +1,55 @@ +import type * as ts from 'typescript'; + +import { getValueOfLiteralType } from '../../src/util/getValueOfLiteralType'; + +describe('getValueOfLiteralType', () => { + it('returns a string for a string literal type', () => { + const stringLiteralType = { + value: 'hello' satisfies string, + } as ts.LiteralType; + + const result = getValueOfLiteralType(stringLiteralType); + + expect(result).toBe('hello'); + expect(typeof result).toBe('string'); + }); + + it('returns a number for a numeric literal type', () => { + const numberLiteralType = { + value: 42 satisfies number, + } as ts.LiteralType; + + const result = getValueOfLiteralType(numberLiteralType); + + expect(result).toBe(42); + expect(typeof result).toBe('number'); + }); + + it('returns a bigint for a pseudo-bigint literal type', () => { + const pseudoBigIntLiteralType = { + value: { + base10Value: '12345678901234567890', + negative: false, + } satisfies ts.PseudoBigInt, + } as ts.LiteralType; + + const result = getValueOfLiteralType(pseudoBigIntLiteralType); + + expect(result).toBe(BigInt('12345678901234567890')); + expect(typeof result).toBe('bigint'); + }); + + it('returns a negative bigint for a pseudo-bigint with negative=true', () => { + const negativePseudoBigIntLiteralType = { + value: { + base10Value: '98765432109876543210', + negative: true, + } satisfies ts.PseudoBigInt, + } as ts.LiteralType; + + const result = getValueOfLiteralType(negativePseudoBigIntLiteralType); + + expect(result).toBe(BigInt('-98765432109876543210')); + expect(typeof result).toBe('bigint'); + }); +}); From d6cc757940006418d3c2ee93a5e308571285646c Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 5 Jan 2025 10:17:03 +0100 Subject: [PATCH 17/33] re-add utils in no-unnecessary-condition for utils coverage --- .../src/rules/no-unnecessary-condition.ts | 62 ++----------------- 1 file changed, 5 insertions(+), 57 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 97664597ef6d..b8b77ef2d530 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -11,9 +11,14 @@ import { getParserServices, getTypeName, getTypeOfPropertyOfName, + getValueOfLiteralType, + isAlwaysNullish, isArrayMethodCallWithPredicate, isIdentifier, isNullableType, + isPossiblyFalsy, + isPossiblyNullish, + isPossiblyTruthy, isTypeAnyType, isTypeFlagSet, isTypeUnknownType, @@ -25,59 +30,7 @@ import { findTypeGuardAssertedArgument, } from '../util/assertionFunctionUtils'; -// Truthiness utilities // #region -const valueIsPseudoBigInt = ( - value: number | string | ts.PseudoBigInt, -): value is ts.PseudoBigInt => { - return typeof value === 'object'; -}; - -const getValueOfLiteralType = ( - type: ts.LiteralType, -): bigint | number | string => { - if (valueIsPseudoBigInt(type.value)) { - return pseudoBigIntToBigInt(type.value); - } - return type.value; -}; - -const isTruthyLiteral = (type: ts.Type): boolean => - tsutils.isTrueLiteralType(type) || - (type.isLiteral() && !!getValueOfLiteralType(type)); - -const isPossiblyFalsy = (type: ts.Type): boolean => - tsutils - .unionTypeParts(type) - // Intersections like `string & {}` can also be possibly falsy, - // requiring us to look into the intersection. - .flatMap(type => tsutils.intersectionTypeParts(type)) - // PossiblyFalsy flag includes literal values, so exclude ones that - // are definitely truthy - .filter(t => !isTruthyLiteral(t)) - .some(type => isTypeFlagSet(type, ts.TypeFlags.PossiblyFalsy)); - -const isPossiblyTruthy = (type: ts.Type): boolean => - tsutils - .unionTypeParts(type) - .map(type => tsutils.intersectionTypeParts(type)) - .some(intersectionParts => - // It is possible to define intersections that are always falsy, - // like `"" & { __brand: string }`. - intersectionParts.every(type => !tsutils.isFalsyType(type)), - ); - -// Nullish utilities -const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null; -const isNullishType = (type: ts.Type): boolean => - isTypeFlagSet(type, nullishFlag); - -const isPossiblyNullish = (type: ts.Type): boolean => - tsutils.unionTypeParts(type).some(isNullishType); - -const isAlwaysNullish = (type: ts.Type): boolean => - tsutils.unionTypeParts(type).every(isNullishType); - function toStaticValue( type: ts.Type, ): @@ -100,10 +53,6 @@ function toStaticValue( return undefined; } -function pseudoBigIntToBigInt(value: ts.PseudoBigInt): bigint { - return BigInt((value.negative ? '-' : '') + value.base10Value); -} - const BOOL_OPERATORS = new Set([ '<', '>', @@ -151,7 +100,6 @@ function booleanComparison( return left >= right; } } - // #endregion export type Options = [ From ec87f639e44767901122511d17da50d214fad161 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 5 Jan 2025 15:49:20 +0100 Subject: [PATCH 18/33] anticipate optional chain handling --- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 8554018de81e..013bb9901d67 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -446,8 +446,8 @@ export default createRule({ fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { const [left, right] = operator === '===' || operator === '==' || operator === '!' - ? [node.alternate, node.consequent] - : [node.consequent, node.alternate]; + ? [identifier, node.consequent] + : [identifier, node.alternate]; return fixer.replaceText( node, `${getTextWithParentheses(context.sourceCode, left)} ?? ${getTextWithParentheses( From 23b095bc441c59087c9cdc3f142235faa34cfdd0 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 19 Jan 2025 01:08:31 +0100 Subject: [PATCH 19/33] align behaviour with || one --- .../src/rules/prefer-nullish-coalescing.ts | 23 +- .../rules/prefer-nullish-coalescing.test.ts | 532 ++++-------------- 2 files changed, 125 insertions(+), 430 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 013bb9901d67..5548559e0092 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -13,6 +13,7 @@ import { isNodeEqual, isNullLiteral, isPossiblyFalsy, + isPossiblyNullish, isTypeFlagSet, isUndefinedIdentifier, nullThrows, @@ -347,7 +348,7 @@ export default createRule({ } } - let identifier: TSESTree.Node | undefined; + let identifierOrMemberExpresion: TSESTree.Node | undefined; let hasUndefinedCheck = false; let hasNullCheck = false; let hasTruthinessCheck = false; @@ -361,14 +362,14 @@ export default createRule({ isIdentifierOrMemberExpressionType(node.test.type) && isNodeEqual(node.test, node.consequent) ) { - identifier = node.test; + identifierOrMemberExpresion = node.test; } else if ( node.test.type === AST_NODE_TYPES.UnaryExpression && node.test.operator === '!' && isIdentifierOrMemberExpressionType(node.test.argument.type) && isNodeEqual(node.test.argument, node.alternate) ) { - identifier = node.test.argument; + identifierOrMemberExpresion = node.test.argument; operator = '!'; } } else { @@ -382,17 +383,17 @@ export default createRule({ (operator === '!==' || operator === '!=') && isNodeEqual(testNode, node.consequent) ) { - identifier = testNode; + identifierOrMemberExpresion = testNode; } else if ( (operator === '===' || operator === '==') && isNodeEqual(testNode, node.alternate) ) { - identifier = testNode; + identifierOrMemberExpresion = testNode; } } } - if (!identifier) { + if (!identifierOrMemberExpresion) { return; } @@ -407,7 +408,9 @@ export default createRule({ return true; } - const tsNode = parserServices.esTreeNodeToTSNodeMap.get(identifier); + const tsNode = parserServices.esTreeNodeToTSNodeMap.get( + identifierOrMemberExpresion, + ); const type = checker.getTypeAtLocation(tsNode); const flags = getTypeFlags(type); @@ -417,7 +420,7 @@ export default createRule({ if (hasTruthinessCheck) { const nonNullishType = checker.getNonNullableType(type); - return !isPossiblyFalsy(nonNullishType); + return isPossiblyNullish(type) && !isPossiblyFalsy(nonNullishType); } const hasNullType = (flags & ts.TypeFlags.Null) !== 0; @@ -446,8 +449,8 @@ export default createRule({ fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { const [left, right] = operator === '===' || operator === '==' || operator === '!' - ? [identifier, node.consequent] - : [identifier, node.alternate]; + ? [identifierOrMemberExpresion, node.consequent] + : [identifierOrMemberExpresion, node.alternate]; return fixer.replaceText( node, `${getTextWithParentheses(context.sourceCode, left)} ?? ${getTextWithParentheses( diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 2a93130da77d..695a29dc244c 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -299,6 +299,62 @@ x ? x : y; `, ` declare let x: unknown; +!x ? y : x; + `, + ` +declare let x: object; +x ? x : y; + `, + ` +declare let x: object; +!x ? y : x; + `, + ` +declare let x: string[]; +x ? x : y; + `, + ` +declare let x: string[]; +!x ? y : x; + `, + ` +declare let x: Function; +x ? x : y; + `, + ` +declare let x: Function; +!x ? y : x; + `, + ` +declare let x: () => string; +x ? x : y; + `, + ` +declare let x: () => string; +!x ? y : x; + `, + ` +declare let x: () => string | null; +x ? x : y; + `, + ` +declare let x: () => string | null; +!x ? y : x; + `, + ` +declare let x: () => string | undefined; +x ? x : y; + `, + ` +declare let x: () => string | undefined; +!x ? y : x; + `, + ` +declare let x: () => string | null | undefined; +x ? x : y; + `, + ` +declare let x: () => string | null | undefined; !x ? y : x; `, ` @@ -477,6 +533,62 @@ x.n ? x.n : y; declare let x: { n: unknown }; !x.n ? y : x.n; `, + ` +declare let x: { n: object }; +x ? x : y; + `, + ` +declare let x: { n: object }; +!x ? y : x; + `, + ` +declare let x: { n: string[] }; +x ? x : y; + `, + ` +declare let x: { n: string[] }; +!x ? y : x; + `, + ` +declare let x: { n: Function }; +x ? x : y; + `, + ` +declare let x: { n: Function }; +!x ? y : x; + `, + ` +declare let x: { n: () => string }; +x ? x : y; + `, + ` +declare let x: { n: () => string }; +!x ? y : x; + `, + ` +declare let x: { n: () => string | null }; +x ? x : y; + `, + ` +declare let x: { n: () => string | null }; +!x ? y : x; + `, + ` +declare let x: { n: () => string | undefined }; +x ? x : y; + `, + ` +declare let x: { n: () => string | undefined }; +!x ? y : x; + `, + ` +declare let x: { n: () => string | null | undefined }; +x ? x : y; + `, + ` +declare let x: { n: () => string | null | undefined }; +!x ? y : x; + `, ].map(code => ({ code, options: [{ ignoreTernaryTests: false }] as const, @@ -1140,14 +1252,6 @@ declare let x: string | null; null === x ? y : x; `, ` -declare let x: string[]; -x ? x : y; - `, - ` -declare let x: string[]; -!x ? y : x; - `, - ` declare let x: string[] | null; x ? x : y; `, @@ -1169,14 +1273,6 @@ x ? x : y; `, ` declare let x: string[] | null | undefined; -!x ? y : x; - `, - ` -declare let x: object; -x ? x : y; - `, - ` -declare let x: object; !x ? y : x; `, ` @@ -1201,14 +1297,6 @@ x ? x : y; `, ` declare let x: object | null | undefined; -!x ? y : x; - `, - ` -declare let x: Function; -x ? x : y; - `, - ` -declare let x: Function; !x ? y : x; `, ` @@ -1233,38 +1321,6 @@ x ? x : y; `, ` declare let x: Function | null | undefined; -!x ? y : x; - `, - ` -declare let x: () => string; -x ? x : y; - `, - ` -declare let x: () => string; -!x ? y : x; - `, - ` -declare let x: () => string | null; -x ? x : y; - `, - ` -declare let x: () => string | null; -!x ? y : x; - `, - ` -declare let x: () => string | undefined; -x ? x : y; - `, - ` -declare let x: () => string | undefined; -!x ? y : x; - `, - ` -declare let x: () => string | null | undefined; -x ? x : y; - `, - ` -declare let x: () => string | null | undefined; !x ? y : x; `, ` @@ -2835,58 +2891,6 @@ if (+(a ?? b)) { }, { code: ` -declare const x: { n: object }; -declare const y: any; - -x.n ? x.n : y; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: object }; -declare const y: any; - -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: object }; -declare const y: any; - -!x.n ? y : x.n; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: object }; -declare const y: any; - -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` declare const x: { n: object | null }; declare const y: any; @@ -3032,58 +3036,6 @@ declare const y: any; declare const x: { n: object | null | undefined }; declare const y: any; -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: string[] }; -declare const y: any; - -x.n ? x.n : y; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: string[] }; -declare const y: any; - -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: string[] }; -declare const y: any; - -!x.n ? y : x.n; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: string[] }; -declare const y: any; - x.n ?? y; `, }, @@ -3240,58 +3192,6 @@ declare const y: any; declare const x: { n: string[] | null | undefined }; declare const y: any; -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: Function }; -declare const y: any; - -x.n ? x.n : y; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: Function }; -declare const y: any; - -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: Function }; -declare const y: any; - -!x.n ? y : x.n; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: Function }; -declare const y: any; - x.n ?? y; `, }, @@ -3448,214 +3348,6 @@ declare const y: any; declare const x: { n: Function | null | undefined }; declare const y: any; -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: () => string }; -declare const y: any; - -x.n ? x.n : y; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: () => string }; -declare const y: any; - -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: () => string }; -declare const y: any; - -!x.n ? y : x.n; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: () => string }; -declare const y: any; - -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: () => string | null }; -declare const y: any; - -x.n ? x.n : y; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: () => string | null }; -declare const y: any; - -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: () => string | null }; -declare const y: any; - -!x.n ? y : x.n; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: () => string | null }; -declare const y: any; - -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: () => string | undefined }; -declare const y: any; - -x.n ? x.n : y; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: () => string | undefined }; -declare const y: any; - -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: () => string | undefined }; -declare const y: any; - -!x.n ? y : x.n; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: () => string | undefined }; -declare const y: any; - -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: () => string | null | undefined }; -declare const y: any; - -x.n ? x.n : y; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: () => string | null | undefined }; -declare const y: any; - -x.n ?? y; - `, - }, - ], - }, - ], - options: [{ ignoreTernaryTests: false }], - output: null, - }, - { - code: ` -declare const x: { n: () => string | null | undefined }; -declare const y: any; - -!x.n ? y : x.n; - `, - errors: [ - { - messageId: 'preferNullishOverTernary', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare const x: { n: () => string | null | undefined }; -declare const y: any; - x.n ?? y; `, }, From 5c6742261674201225e9dbb328b0eee631b72d9e Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 19 Jan 2025 01:52:04 +0100 Subject: [PATCH 20/33] simplify --- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 5548559e0092..2d1d4c00f2fd 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -192,7 +192,7 @@ export default createRule({ ): void { const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); const type = checker.getTypeAtLocation(tsNode.left); - if (!isTypeFlagSet(type, ts.TypeFlags.Null | ts.TypeFlags.Undefined)) { + if (!isPossiblyNullish(type)) { return; } From 313f5e4052c3259a7f0b51b45ae7e1b6930865b9 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 19 Jan 2025 09:17:43 +0100 Subject: [PATCH 21/33] fix eslint --- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 2d1d4c00f2fd..ac0f546486ef 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -14,7 +14,6 @@ import { isNullLiteral, isPossiblyFalsy, isPossiblyNullish, - isTypeFlagSet, isUndefinedIdentifier, nullThrows, NullThrowsReasons, From 24424ead3a01d5dd7872070af4700b78eadcf1af Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 19 Jan 2025 21:23:25 +0100 Subject: [PATCH 22/33] align behaviour on || one --- .../docs/rules/prefer-nullish-coalescing.mdx | 16 +- .../src/rules/prefer-nullish-coalescing.ts | 89 +- .../prefer-nullish-coalescing.shot | 16 +- .../rules/prefer-nullish-coalescing.test.ts | 2435 +++++++---------- 4 files changed, 1022 insertions(+), 1534 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx index 5df1164ae7c3..5e566e2e49b8 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx @@ -42,13 +42,9 @@ foo === undefined ? 'a string' : foo; const foo: string | null = 'bar'; foo !== null ? foo : 'a string'; +foo ? foo : 'a string'; foo === null ? 'a string' : foo; - -const foo: object | null = { value: 'bar' }; -foo !== null ? foo : { value: 'a string' }; -foo === null ? { value: 'a string' } : foo; -foo ? foo : { value: 'a string' }; -!foo ? { value: 'a string' } : foo; +!foo ? 'a string' : foo; ``` Correct code for `ignoreTernaryTests: false`: @@ -67,12 +63,8 @@ foo ?? 'a string'; const foo: string | null = 'bar'; foo ?? 'a string'; foo ?? 'a string'; - -const foo: object | null = { value: 'bar' }; -foo ?? { value: 'a string' }; -foo ?? { value: 'a string' }; -foo ?? { value: 'a string' }; -foo ?? { value: 'a string' }; +foo ?? 'a string'; +foo ?? 'a string'; ``` ### `ignoreConditionalTests` diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index ac0f546486ef..f561d146d123 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -12,7 +12,6 @@ import { isLogicalOrOperator, isNodeEqual, isNullLiteral, - isPossiblyFalsy, isPossiblyNullish, isUndefinedIdentifier, nullThrows, @@ -22,6 +21,41 @@ import { const isIdentifierOrMemberExpressionType = (type: TSESTree.AST_NODE_TYPES) => [AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression].includes(type); +function ignore( + type: ts.Type, + ignorePrimitives: Options[0]['ignorePrimitives'], +): boolean { + if (!isPossiblyNullish(type)) { + return true; + } + const ignorableFlags = [ + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + (ignorePrimitives === true || ignorePrimitives!.bigint) && + ts.TypeFlags.BigIntLike, + (ignorePrimitives === true || ignorePrimitives!.boolean) && + ts.TypeFlags.BooleanLike, + (ignorePrimitives === true || ignorePrimitives!.number) && + ts.TypeFlags.NumberLike, + (ignorePrimitives === true || ignorePrimitives!.string) && + ts.TypeFlags.StringLike, + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + ] + .filter((flag): flag is number => typeof flag === 'number') + .reduce((previous, flag) => previous | flag, 0); + if ( + type.flags !== ts.TypeFlags.Null && + type.flags !== ts.TypeFlags.Undefined && + (type as ts.UnionOrIntersectionType).types.some(t => + tsutils + .intersectionTypeParts(t) + .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)), + ) + ) { + return true; + } + return false; +} + export type Options = [ { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; @@ -191,7 +225,7 @@ export default createRule({ ): void { const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); const type = checker.getTypeAtLocation(tsNode.left); - if (!isPossiblyNullish(type)) { + if (ignore(type, ignorePrimitives)) { return; } @@ -206,33 +240,6 @@ export default createRule({ return; } - // https://github.com/typescript-eslint/typescript-eslint/issues/5439 - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - const ignorableFlags = [ - (ignorePrimitives === true || ignorePrimitives!.bigint) && - ts.TypeFlags.BigIntLike, - (ignorePrimitives === true || ignorePrimitives!.boolean) && - ts.TypeFlags.BooleanLike, - (ignorePrimitives === true || ignorePrimitives!.number) && - ts.TypeFlags.NumberLike, - (ignorePrimitives === true || ignorePrimitives!.string) && - ts.TypeFlags.StringLike, - ] - .filter((flag): flag is number => typeof flag === 'number') - .reduce((previous, flag) => previous | flag, 0); - if ( - type.flags !== ts.TypeFlags.Null && - type.flags !== ts.TypeFlags.Undefined && - (type as ts.UnionOrIntersectionType).types.some(t => - tsutils - .intersectionTypeParts(t) - .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)), - ) - ) { - return; - } - /* eslint-enable @typescript-eslint/no-non-null-assertion */ - const barBarOperator = nullThrows( context.sourceCode.getTokenAfter( node.left, @@ -397,8 +404,19 @@ export default createRule({ } const isFixable = ((): boolean => { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get( + identifierOrMemberExpresion, + ); + const type = checker.getTypeAtLocation(tsNode); + + // x ? x : y and !x ? y : x patterns + if (hasTruthinessCheck) { + return !ignore(type, ignorePrimitives); + } + + const flags = getTypeFlags(type); // it is fixable if we check for both null and undefined, or not if neither - if (!hasTruthinessCheck && hasUndefinedCheck === hasNullCheck) { + if (hasUndefinedCheck === hasNullCheck) { return hasUndefinedCheck; } @@ -407,21 +425,10 @@ export default createRule({ return true; } - const tsNode = parserServices.esTreeNodeToTSNodeMap.get( - identifierOrMemberExpresion, - ); - const type = checker.getTypeAtLocation(tsNode); - const flags = getTypeFlags(type); - if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { return false; } - if (hasTruthinessCheck) { - const nonNullishType = checker.getNonNullableType(type); - return isPossiblyNullish(type) && !isPossiblyFalsy(nonNullishType); - } - const hasNullType = (flags & ts.TypeFlags.Null) !== 0; // it is fixable if we check for undefined and the type is not nullable diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot index a0160c8e7774..aeaefe9dc5ff 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot @@ -19,13 +19,9 @@ foo === undefined ? 'a string' : foo; const foo: string | null = 'bar'; foo !== null ? foo : 'a string'; +foo ? foo : 'a string'; foo === null ? 'a string' : foo; - -const foo: object | null = { value: 'bar' }; -foo !== null ? foo : { value: 'a string' }; -foo === null ? { value: 'a string' } : foo; -foo ? foo : { value: 'a string' }; -!foo ? { value: 'a string' } : foo; +!foo ? 'a string' : foo; " `; @@ -45,12 +41,8 @@ foo ?? 'a string'; const foo: string | null = 'bar'; foo ?? 'a string'; foo ?? 'a string'; - -const foo: object | null = { value: 'bar' }; -foo ?? { value: 'a string' }; -foo ?? { value: 'a string' }; -foo ?? { value: 'a string' }; -foo ?? { value: 'a string' }; +foo ?? 'a string'; +foo ?? 'a string'; " `; diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 695a29dc244c..fed5a063b090 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -131,30 +131,6 @@ x ? x : y; `, ` declare let x: string; -!x ? y : x; - `, - ` -declare let x: string | null; -x ? x : y; - `, - ` -declare let x: string | null; -!x ? y : x; - `, - ` -declare let x: string | undefined; -x ? x : y; - `, - ` -declare let x: string | undefined; -!x ? y : x; - `, - ` -declare let x: string | null | undefined; -x ? x : y; - `, - ` -declare let x: string | null | undefined; !x ? y : x; `, ` @@ -163,30 +139,6 @@ x ? x : y; `, ` declare let x: string | object; -!x ? y : x; - `, - ` -declare let x: string | object | null; -x ? x : y; - `, - ` -declare let x: string | object | null; -!x ? y : x; - `, - ` -declare let x: string | object | undefined; -x ? x : y; - `, - ` -declare let x: string | object | undefined; -!x ? y : x; - `, - ` -declare let x: string | object | null | undefined; -x ? x : y; - `, - ` -declare let x: string | object | null | undefined; !x ? y : x; `, ` @@ -195,30 +147,6 @@ x ? x : y; `, ` declare let x: number; -!x ? y : x; - `, - ` -declare let x: number | null; -x ? x : y; - `, - ` -declare let x: number | null; -!x ? y : x; - `, - ` -declare let x: number | undefined; -x ? x : y; - `, - ` -declare let x: number | undefined; -!x ? y : x; - `, - ` -declare let x: number | null | undefined; -x ? x : y; - `, - ` -declare let x: number | null | undefined; !x ? y : x; `, ` @@ -227,30 +155,6 @@ x ? x : y; `, ` declare let x: bigint; -!x ? y : x; - `, - ` -declare let x: bigint | null; -x ? x : y; - `, - ` -declare let x: bigint | null; -!x ? y : x; - `, - ` -declare let x: bigint | undefined; -x ? x : y; - `, - ` -declare let x: bigint | undefined; -!x ? y : x; - `, - ` -declare let x: bigint | null | undefined; -x ? x : y; - `, - ` -declare let x: bigint | null | undefined; !x ? y : x; `, ` @@ -259,30 +163,6 @@ x ? x : y; `, ` declare let x: boolean; -!x ? y : x; - `, - ` -declare let x: boolean | null; -x ? x : y; - `, - ` -declare let x: boolean | null; -!x ? y : x; - `, - ` -declare let x: boolean | undefined; -x ? x : y; - `, - ` -declare let x: boolean | undefined; -!x ? y : x; - `, - ` -declare let x: boolean | null | undefined; -x ? x : y; - `, - ` -declare let x: boolean | null | undefined; !x ? y : x; `, ` @@ -363,30 +243,6 @@ x.n ? x.n : y; `, ` declare let x: { n: string }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: string | null }; -x.n ? x.n : y; - `, - ` -declare let x: { n: string | null }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: string | undefined }; -x.n ? x.n : y; - `, - ` -declare let x: { n: string | undefined }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: string | null | undefined }; -x.n ? x.n : y; - `, - ` -declare let x: { n: string | null | undefined }; !x.n ? y : x.n; `, ` @@ -395,30 +251,6 @@ x.n ? x.n : y; `, ` declare let x: { n: string | object }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: string | object | null }; -x.n ? x.n : y; - `, - ` -declare let x: { n: string | object | null }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: string | object | undefined }; -x.n ? x.n : y; - `, - ` -declare let x: { n: string | object | undefined }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: string | object | null | undefined }; -x.n ? x.n : y; - `, - ` -declare let x: { n: string | object | null | undefined }; !x.n ? y : x.n; `, ` @@ -427,30 +259,6 @@ x.n ? x.n : y; `, ` declare let x: { n: number }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: number | null }; -x.n ? x.n : y; - `, - ` -declare let x: { n: number | null }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: number | undefined }; -x.n ? x.n : y; - `, - ` -declare let x: { n: number | undefined }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: number | null | undefined }; -x.n ? x.n : y; - `, - ` -declare let x: { n: number | null | undefined }; !x.n ? y : x.n; `, ` @@ -459,30 +267,6 @@ x.n ? x.n : y; `, ` declare let x: { n: bigint }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: bigint | null }; -x.n ? x.n : y; - `, - ` -declare let x: { n: bigint | null }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: bigint | undefined }; -x.n ? x.n : y; - `, - ` -declare let x: { n: bigint | undefined }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: bigint | null | undefined }; -x.n ? x.n : y; - `, - ` -declare let x: { n: bigint | null | undefined }; !x.n ? y : x.n; `, ` @@ -491,30 +275,6 @@ x.n ? x.n : y; `, ` declare let x: { n: boolean }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: boolean | null }; -x.n ? x.n : y; - `, - ` -declare let x: { n: boolean | null }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: boolean | undefined }; -x.n ? x.n : y; - `, - ` -declare let x: { n: boolean | undefined }; -!x.n ? y : x.n; - `, - ` -declare let x: { n: boolean | null | undefined }; -x.n ? x.n : y; - `, - ` -declare let x: { n: boolean | null | undefined }; !x.n ? y : x.n; `, ` @@ -1252,35 +1012,155 @@ declare let x: string | null; null === x ? y : x; `, ` -declare let x: string[] | null; +declare let x: string | null; x ? x : y; `, ` -declare let x: string[] | null; +declare let x: string | null; !x ? y : x; `, ` -declare let x: string[] | undefined; +declare let x: string | undefined; x ? x : y; `, ` -declare let x: string[] | undefined; +declare let x: string | undefined; !x ? y : x; `, ` -declare let x: string[] | null | undefined; +declare let x: string | null | undefined; x ? x : y; `, ` -declare let x: string[] | null | undefined; +declare let x: string | null | undefined; !x ? y : x; `, ` -declare let x: object | null; +declare let x: string | object | null; x ? x : y; `, ` -declare let x: object | null; +declare let x: string | object | null; +!x ? y : x; + `, + ` +declare let x: string | object | undefined; +x ? x : y; + `, + ` +declare let x: string | object | undefined; +!x ? y : x; + `, + ` +declare let x: string | object | null | undefined; +x ? x : y; + `, + ` +declare let x: string | object | null | undefined; +!x ? y : x; + `, + ` +declare let x: number | null; +x ? x : y; + `, + ` +declare let x: number | null; +!x ? y : x; + `, + ` +declare let x: number | undefined; +x ? x : y; + `, + ` +declare let x: number | undefined; +!x ? y : x; + `, + ` +declare let x: number | null | undefined; +x ? x : y; + `, + ` +declare let x: number | null | undefined; +!x ? y : x; + `, + ` +declare let x: bigint | null; +x ? x : y; + `, + ` +declare let x: bigint | null; +!x ? y : x; + `, + ` +declare let x: bigint | undefined; +x ? x : y; + `, + ` +declare let x: bigint | undefined; +!x ? y : x; + `, + ` +declare let x: bigint | null | undefined; +x ? x : y; + `, + ` +declare let x: bigint | null | undefined; +!x ? y : x; + `, + ` +declare let x: boolean | null; +x ? x : y; + `, + ` +declare let x: boolean | null; +!x ? y : x; + `, + ` +declare let x: boolean | undefined; +x ? x : y; + `, + ` +declare let x: boolean | undefined; +!x ? y : x; + `, + ` +declare let x: boolean | null | undefined; +x ? x : y; + `, + ` +declare let x: boolean | null | undefined; +!x ? y : x; + `, + ` +declare let x: string[] | null; +x ? x : y; + `, + ` +declare let x: string[] | null; +!x ? y : x; + `, + ` +declare let x: string[] | undefined; +x ? x : y; + `, + ` +declare let x: string[] | undefined; +!x ? y : x; + `, + ` +declare let x: string[] | null | undefined; +x ? x : y; + `, + ` +declare let x: string[] | null | undefined; +!x ? y : x; + `, + ` +declare let x: object | null; +x ? x : y; + `, + ` +declare let x: object | null; !x ? y : x; `, ` @@ -1371,1094 +1251,617 @@ x ?? y; output: null, })), - // noStrictNullCheck - { - code: ` -declare let x: string[] | null; -if (x) { -} + ...[ + ` +declare let x: { n: string | null }; +x.n ? x.n : y; `, - errors: [ - { - column: 1, - line: 0, - messageId: 'noStrictNullCheck', - }, - ], - languageOptions: { - parserOptions: { - tsconfigRootDir: path.join(rootPath, 'unstrict'), - }, - }, - output: null, - }, - - // ignoreConditionalTests - ...nullishTypeTest((nullish, type, equals) => ({ - code: ` -declare let x: ${type} | ${nullish}; -(x ||${equals} 'foo') ? null : null; + ` +declare let x: { n: string | null }; +!x.n ? y : x.n; `, - errors: [ - { - column: 4, - endColumn: 6 + equals.length, - endLine: 3, - line: 3, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: ${type} | ${nullish}; -(x ??${equals} 'foo') ? null : null; + ` +declare let x: { n: string | undefined }; +x.n ? x.n : y; `, - }, - ], - }, - ], - options: [{ ignoreConditionalTests: false }], - output: null, - })), - ...nullishTypeTest((nullish, type, equals) => ({ - code: ` -declare let x: ${type} | ${nullish}; -if ((x ||${equals} 'foo')) {} + ` +declare let x: { n: string | undefined }; +!x.n ? y : x.n; `, - errors: [ - { - column: 8, - endColumn: 10 + equals.length, - endLine: 3, - line: 3, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: ${type} | ${nullish}; -if ((x ??${equals} 'foo')) {} + ` +declare let x: { n: string | null | undefined }; +x.n ? x.n : y; `, - }, - ], - }, - ], - options: [{ ignoreConditionalTests: false }], - output: null, - })), - ...nullishTypeTest((nullish, type, equals) => ({ - code: ` -declare let x: ${type} | ${nullish}; -do {} while ((x ||${equals} 'foo')) + ` +declare let x: { n: string | null | undefined }; +!x.n ? y : x.n; `, - errors: [ - { - column: 17, - endColumn: 19 + equals.length, - endLine: 3, - line: 3, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: ${type} | ${nullish}; -do {} while ((x ??${equals} 'foo')) + ` +declare let x: { n: string | object | null }; +x.n ? x.n : y; `, - }, - ], - }, - ], - options: [{ ignoreConditionalTests: false }], - output: null, - })), - ...nullishTypeTest((nullish, type, equals) => ({ - code: ` -declare let x: ${type} | ${nullish}; -for (;(x ||${equals} 'foo');) {} + ` +declare let x: { n: string | object | null }; +!x.n ? y : x.n; `, - errors: [ - { - column: 10, - endColumn: 12 + equals.length, - endLine: 3, - line: 3, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: ${type} | ${nullish}; -for (;(x ??${equals} 'foo');) {} + ` +declare let x: { n: string | object | undefined }; +x.n ? x.n : y; `, - }, - ], - }, - ], - options: [{ ignoreConditionalTests: false }], - output: null, - })), - ...nullishTypeTest((nullish, type, equals) => ({ - code: ` -declare let x: ${type} | ${nullish}; -while ((x ||${equals} 'foo')) {} + ` +declare let x: { n: string | object | undefined }; +!x.n ? y : x.n; `, - errors: [ - { - column: 11, - endColumn: 13 + equals.length, - endLine: 3, - line: 3, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: ${type} | ${nullish}; -while ((x ??${equals} 'foo')) {} + ` +declare let x: { n: string | object | null | undefined }; +x.n ? x.n : y; `, - }, - ], - }, - ], - options: [{ ignoreConditionalTests: false }], - output: null, - })), - - // ignoreMixedLogicalExpressions - ...nullishTypeTest((nullish, type) => ({ - code: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -a || b && c; + ` +declare let x: { n: string | object | null | undefined }; +!x.n ? y : x.n; `, - errors: [ - { - column: 3, - endColumn: 5, - endLine: 5, - line: 5, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -a ?? b && c; + ` +declare let x: { n: number | null }; +x.n ? x.n : y; `, - }, - ], - }, - ], - options: [{ ignoreMixedLogicalExpressions: false }], - })), - ...nullishTypeTest((nullish, type) => ({ - code: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -declare let d: ${type} | ${nullish}; -a || b || c && d; + ` +declare let x: { n: number | null }; +!x.n ? y : x.n; `, - errors: [ - { - column: 3, - endColumn: 5, - endLine: 6, - line: 6, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -declare let d: ${type} | ${nullish}; -(a ?? b) || c && d; + ` +declare let x: { n: number | undefined }; +x.n ? x.n : y; `, - }, - ], - }, - { - column: 8, - endColumn: 10, - endLine: 6, - line: 6, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -declare let d: ${type} | ${nullish}; -a || b ?? c && d; + ` +declare let x: { n: number | undefined }; +!x.n ? y : x.n; `, - }, - ], - }, - ], - options: [{ ignoreMixedLogicalExpressions: false }], - })), - ...nullishTypeTest((nullish, type) => ({ - code: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -declare let d: ${type} | ${nullish}; -a && b || c || d; + ` +declare let x: { n: number | null | undefined }; +x.n ? x.n : y; `, - errors: [ - { - column: 8, - endColumn: 10, - endLine: 6, - line: 6, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -declare let d: ${type} | ${nullish}; -a && (b ?? c) || d; + ` +declare let x: { n: number | null | undefined }; +!x.n ? y : x.n; `, - }, - ], - }, - { - column: 13, - endColumn: 15, - endLine: 6, - line: 6, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type} | ${nullish}; -declare let c: ${type} | ${nullish}; -declare let d: ${type} | ${nullish}; -a && b || c ?? d; + ` +declare let x: { n: bigint | null }; +x.n ? x.n : y; `, - }, - ], - }, - ], - options: [{ ignoreMixedLogicalExpressions: false }], - })), - - // should not false positive for functions inside conditional tests - ...nullishTypeTest((nullish, type, equals) => ({ - code: ` -declare let x: ${type} | ${nullish}; -if (() => (x ||${equals} 'foo')) {} + ` +declare let x: { n: bigint | null }; +!x.n ? y : x.n; `, - errors: [ - { - column: 14, - endColumn: 16 + equals.length, - endLine: 3, - line: 3, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: ${type} | ${nullish}; -if (() => (x ??${equals} 'foo')) {} + ` +declare let x: { n: bigint | undefined }; +x.n ? x.n : y; `, - }, - ], - }, - ], - output: null, - })), - ...nullishTypeTest((nullish, type, equals) => ({ - code: ` -declare let x: ${type} | ${nullish}; -if (function weird() { return (x ||${equals} 'foo') }) {} + ` +declare let x: { n: bigint | undefined }; +!x.n ? y : x.n; `, - errors: [ - { - column: 34, - endColumn: 36 + equals.length, - endLine: 3, - line: 3, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: ${type} | ${nullish}; -if (function weird() { return (x ??${equals} 'foo') }) {} + ` +declare let x: { n: bigint | null | undefined }; +x.n ? x.n : y; `, - }, - ], - }, - ], - output: null, - })), - // https://github.com/typescript-eslint/typescript-eslint/issues/1290 - ...nullishTypeTest((nullish, type) => ({ - code: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type}; -declare let c: ${type}; -a || b || c; + ` +declare let x: { n: bigint | null | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: boolean | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: boolean | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: boolean | undefined }; +x.n ? x.n : y; `, - errors: [ - { - column: 3, - endColumn: 5, - endLine: 5, - line: 5, - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type}; -declare let c: ${type}; -(a ?? b) || c; + ` +declare let x: { n: boolean | undefined }; +!x.n ? y : x.n; `, - }, - ], - }, - ], - output: null, - })), - // default for missing option - { - code: ` -declare let x: string | undefined; -x || y; + ` +declare let x: { n: boolean | null | undefined }; +x.n ? x.n : y; `, - errors: [ - { - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: string | undefined; -x ?? y; + ` +declare let x: { n: boolean | null | undefined }; +!x.n ? y : x.n; `, - }, - ], - }, - ], - options: [ - { - ignorePrimitives: { bigint: true, boolean: true, number: true }, - }, - ], - output: null, - }, - { - code: ` -declare let x: number | undefined; -x || y; + ` +declare let x: { n: string[] | null }; +x.n ? x.n : y; `, - errors: [ - { - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: number | undefined; -x ?? y; + ` +declare let x: { n: string[] | null }; +!x.n ? y : x.n; `, - }, - ], - }, - ], - options: [ - { - ignorePrimitives: { bigint: true, boolean: true, string: true }, - }, - ], - output: null, - }, - { - code: ` -declare let x: boolean | undefined; -x || y; + ` +declare let x: { n: string[] | undefined }; +x.n ? x.n : y; `, - errors: [ - { - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: boolean | undefined; -x ?? y; + ` +declare let x: { n: string[] | undefined }; +!x.n ? y : x.n; `, - }, - ], - }, - ], - options: [ - { - ignorePrimitives: { bigint: true, number: true, string: true }, - }, - ], - output: null, - }, - { - code: ` -declare let x: bigint | undefined; -x || y; + ` +declare let x: { n: string[] | null | undefined }; +x.n ? x.n : y; `, - errors: [ - { - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: bigint | undefined; -x ?? y; + ` +declare let x: { n: string[] | null | undefined }; +!x.n ? y : x.n; `, - }, - ], - }, - ], - options: [ - { - ignorePrimitives: { boolean: true, number: true, string: true }, - }, - ], - output: null, - }, - // falsy - { - code: ` -declare let x: '' | undefined; -x || y; + ` +declare let x: { n: object | null }; +x.n ? x.n : y; `, - errors: [ - { - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: '' | undefined; -x ?? y; + ` +declare let x: { n: object | null }; +!x.n ? y : x.n; `, - }, - ], - }, - ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: true, - number: true, - string: false, - }, - }, - ], - output: null, - }, - { - code: ` -declare let x: \`\` | undefined; -x || y; + ` +declare let x: { n: object | undefined }; +x.n ? x.n : y; `, - errors: [ - { - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: \`\` | undefined; -x ?? y; + ` +declare let x: { n: object | undefined }; +!x.n ? y : x.n; `, - }, - ], - }, - ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: true, - number: true, - string: false, - }, - }, - ], - output: null, - }, - { - code: ` -declare let x: 0 | undefined; -x || y; + ` +declare let x: { n: object | null | undefined }; +x.n ? x.n : y; `, - errors: [ - { - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: 0 | undefined; -x ?? y; + ` +declare let x: { n: object | null | undefined }; +!x.n ? y : x.n; `, - }, - ], - }, - ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: true, - number: false, - string: true, - }, - }, - ], - output: null, - }, - { - code: ` -declare let x: 0n | undefined; -x || y; + ` +declare let x: { n: Function | null }; +x.n ? x.n : y; + `, + ` +declare let x: { n: Function | null }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: Function | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: Function | undefined }; +!x.n ? y : x.n; `, - errors: [ - { - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: 0n | undefined; -x ?? y; + ` +declare let x: { n: Function | null | undefined }; +x.n ? x.n : y; `, - }, - ], - }, - ], - options: [ - { - ignorePrimitives: { - bigint: false, - boolean: true, - number: true, - string: true, - }, - }, - ], - output: null, - }, - { - code: ` -declare let x: false | undefined; -x || y; + ` +declare let x: { n: Function | null | undefined }; +!x.n ? y : x.n; `, - errors: [ - { - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: false | undefined; -x ?? y; + ` +declare let x: { n: (() => string) | null }; +x.n ? x.n : y; `, - }, - ], - }, - ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: false, - number: true, - string: true, - }, - }, - ], - output: null, - }, - // truthy - { - code: ` -declare let x: 'a' | undefined; -x || y; + ` +declare let x: { n: (() => string) | null }; +!x.n ? y : x.n; `, - errors: [ - { - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: 'a' | undefined; -x ?? y; + ` +declare let x: { n: (() => string) | undefined }; +x.n ? x.n : y; `, - }, - ], - }, - ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: true, - number: true, - string: false, - }, - }, - ], - output: null, - }, - { - code: ` -declare let x: \`hello\${'string'}\` | undefined; -x || y; + ` +declare let x: { n: (() => string) | undefined }; +!x.n ? y : x.n; + `, + ` +declare let x: { n: (() => string) | null | undefined }; +x.n ? x.n : y; + `, + ` +declare let x: { n: (() => string) | null | undefined }; +!x.n ? y : x.n; `, + ].map(code => ({ + code, errors: [ { - messageId: 'preferNullishOverOr', + column: 1, + endColumn: code.split('\n')[2].length, + endLine: 3, + line: 3, + messageId: 'preferNullishOverTernary' as const, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullish' as const, output: ` -declare let x: \`hello\${'string'}\` | undefined; -x ?? y; +${code.split('\n')[1]} +x.n ?? y; `, }, ], }, ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: true, - number: true, - string: false, - }, - }, - ], + options: [{ ignoreTernaryTests: false }] as const, output: null, - }, + })), + + // noStrictNullCheck { code: ` -declare let x: 1 | undefined; -x || y; +declare let x: string[] | null; +if (x) { +} `, errors: [ { - messageId: 'preferNullishOverOr', - suggestions: [ - { - messageId: 'suggestNullish', - output: ` -declare let x: 1 | undefined; -x ?? y; - `, - }, - ], + column: 1, + line: 0, + messageId: 'noStrictNullCheck', }, ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: true, - number: false, - string: true, - }, + languageOptions: { + parserOptions: { + tsconfigRootDir: path.join(rootPath, 'unstrict'), }, - ], + }, output: null, }, - { + + // ignoreConditionalTests + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare let x: 1n | undefined; -x || y; +declare let x: ${type} | ${nullish}; +(x ||${equals} 'foo') ? null : null; `, errors: [ { + column: 4, + endColumn: 6 + equals.length, + endLine: 3, + line: 3, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 1n | undefined; -x ?? y; +declare let x: ${type} | ${nullish}; +(x ??${equals} 'foo') ? null : null; `, }, ], }, ], - options: [ - { - ignorePrimitives: { - bigint: false, - boolean: true, - number: true, - string: true, - }, - }, - ], + options: [{ ignoreConditionalTests: false }], output: null, - }, - { + })), + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare let x: true | undefined; -x || y; +declare let x: ${type} | ${nullish}; +if ((x ||${equals} 'foo')) {} `, errors: [ { + column: 8, + endColumn: 10 + equals.length, + endLine: 3, + line: 3, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: true | undefined; -x ?? y; +declare let x: ${type} | ${nullish}; +if ((x ??${equals} 'foo')) {} `, }, ], }, ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: false, - number: true, - string: true, - }, - }, - ], + options: [{ ignoreConditionalTests: false }], output: null, - }, - // Unions of same primitive - { + })), + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare let x: 'a' | 'b' | undefined; -x || y; +declare let x: ${type} | ${nullish}; +do {} while ((x ||${equals} 'foo')) `, errors: [ { + column: 17, + endColumn: 19 + equals.length, + endLine: 3, + line: 3, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 'a' | 'b' | undefined; -x ?? y; +declare let x: ${type} | ${nullish}; +do {} while ((x ??${equals} 'foo')) `, }, ], }, ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: true, - number: true, - string: false, - }, - }, - ], + options: [{ ignoreConditionalTests: false }], output: null, - }, - { + })), + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare let x: 'a' | \`b\` | undefined; -x || y; +declare let x: ${type} | ${nullish}; +for (;(x ||${equals} 'foo');) {} `, errors: [ { + column: 10, + endColumn: 12 + equals.length, + endLine: 3, + line: 3, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 'a' | \`b\` | undefined; -x ?? y; +declare let x: ${type} | ${nullish}; +for (;(x ??${equals} 'foo');) {} `, }, ], }, ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: true, - number: true, - string: false, - }, - }, - ], + options: [{ ignoreConditionalTests: false }], output: null, - }, - { + })), + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare let x: 0 | 1 | undefined; -x || y; +declare let x: ${type} | ${nullish}; +while ((x ||${equals} 'foo')) {} `, errors: [ { + column: 11, + endColumn: 13 + equals.length, + endLine: 3, + line: 3, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 0 | 1 | undefined; -x ?? y; +declare let x: ${type} | ${nullish}; +while ((x ??${equals} 'foo')) {} `, }, ], }, ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: true, - number: false, - string: true, - }, - }, - ], + options: [{ ignoreConditionalTests: false }], output: null, - }, - { + })), + + // ignoreMixedLogicalExpressions + ...nullishTypeTest((nullish, type) => ({ code: ` -declare let x: 1 | 2 | 3 | undefined; -x || y; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +a || b && c; `, errors: [ { + column: 3, + endColumn: 5, + endLine: 5, + line: 5, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 1 | 2 | 3 | undefined; -x ?? y; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +a ?? b && c; `, }, ], }, ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: true, - number: false, - string: true, - }, - }, - ], - output: null, - }, - { + options: [{ ignoreMixedLogicalExpressions: false }], + })), + ...nullishTypeTest((nullish, type) => ({ code: ` -declare let x: 0n | 1n | undefined; -x || y; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a || b || c && d; `, errors: [ { + column: 3, + endColumn: 5, + endLine: 6, + line: 6, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 0n | 1n | undefined; -x ?? y; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +(a ?? b) || c && d; `, }, ], }, - ], - options: [ - { - ignorePrimitives: { - bigint: false, - boolean: true, - number: true, - string: true, - }, - }, - ], - output: null, - }, - { - code: ` -declare let x: 1n | 2n | 3n | undefined; -x || y; - `, - errors: [ { + column: 8, + endColumn: 10, + endLine: 6, + line: 6, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 1n | 2n | 3n | undefined; -x ?? y; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a || b ?? c && d; `, }, ], }, ], - options: [ - { - ignorePrimitives: { - bigint: false, - boolean: true, - number: true, - string: true, - }, - }, - ], - output: null, - }, - { + options: [{ ignoreMixedLogicalExpressions: false }], + })), + ...nullishTypeTest((nullish, type) => ({ code: ` -declare let x: true | false | undefined; -x || y; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a && b || c || d; `, errors: [ { + column: 8, + endColumn: 10, + endLine: 6, + line: 6, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: true | false | undefined; -x ?? y; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a && (b ?? c) || d; `, }, ], }, - ], - options: [ { - ignorePrimitives: { - bigint: true, - boolean: false, - number: true, - string: true, - }, + column: 13, + endColumn: 15, + endLine: 6, + line: 6, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a && b || c ?? d; + `, + }, + ], }, ], - output: null, - }, - // Mixed unions - { + options: [{ ignoreMixedLogicalExpressions: false }], + })), + + // should not false positive for functions inside conditional tests + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare let x: 0 | 1 | 0n | 1n | undefined; -x || y; +declare let x: ${type} | ${nullish}; +if (() => (x ||${equals} 'foo')) {} `, errors: [ { + column: 14, + endColumn: 16 + equals.length, + endLine: 3, + line: 3, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 0 | 1 | 0n | 1n | undefined; -x ?? y; +declare let x: ${type} | ${nullish}; +if (() => (x ??${equals} 'foo')) {} `, }, ], }, ], - options: [ - { - ignorePrimitives: { - bigint: false, - boolean: true, - number: false, - string: true, - }, - }, - ], output: null, - }, - { + })), + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare let x: true | false | null | undefined; -x || y; +declare let x: ${type} | ${nullish}; +if (function weird() { return (x ||${equals} 'foo') }) {} `, errors: [ { + column: 34, + endColumn: 36 + equals.length, + endLine: 3, + line: 3, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: true | false | null | undefined; -x ?? y; +declare let x: ${type} | ${nullish}; +if (function weird() { return (x ??${equals} 'foo') }) {} `, }, ], }, ], - options: [ - { - ignorePrimitives: { - bigint: true, - boolean: false, - number: true, - string: true, - }, - }, - ], output: null, - }, - { - code: ` -declare let x: null; -x || y; + })), + // https://github.com/typescript-eslint/typescript-eslint/issues/1290 + ...nullishTypeTest((nullish, type) => ({ + code: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type}; +declare let c: ${type}; +a || b || c; `, errors: [ { + column: 3, + endColumn: 5, + endLine: 5, + line: 5, messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: null; -x ?? y; +declare let a: ${type} | ${nullish}; +declare let b: ${type}; +declare let c: ${type}; +(a ?? b) || c; `, }, ], }, ], output: null, - }, + })), + // default for missing option { code: ` -const x = undefined; +declare let x: string | undefined; x || y; `, errors: [ @@ -2468,18 +1871,24 @@ x || y; { messageId: 'suggestNullish', output: ` -const x = undefined; +declare let x: string | undefined; x ?? y; `, }, ], }, ], + options: [ + { + ignorePrimitives: { bigint: true, boolean: true, number: true }, + }, + ], output: null, }, { code: ` -null || y; +declare let x: number | undefined; +x || y; `, errors: [ { @@ -2488,17 +1897,24 @@ null || y; { messageId: 'suggestNullish', output: ` -null ?? y; +declare let x: number | undefined; +x ?? y; `, }, ], }, ], + options: [ + { + ignorePrimitives: { bigint: true, boolean: true, string: true }, + }, + ], output: null, }, { code: ` -undefined || y; +declare let x: boolean | undefined; +x || y; `, errors: [ { @@ -2507,22 +1923,23 @@ undefined || y; { messageId: 'suggestNullish', output: ` -undefined ?? y; +declare let x: boolean | undefined; +x ?? y; `, }, ], }, ], + options: [ + { + ignorePrimitives: { bigint: true, number: true, string: true }, + }, + ], output: null, }, { code: ` -enum Enum { - A = 0, - B = 1, - C = 2, -} -declare let x: Enum | undefined; +declare let x: bigint | undefined; x || y; `, errors: [ @@ -2532,28 +1949,24 @@ x || y; { messageId: 'suggestNullish', output: ` -enum Enum { - A = 0, - B = 1, - C = 2, -} -declare let x: Enum | undefined; +declare let x: bigint | undefined; x ?? y; `, }, ], }, ], + options: [ + { + ignorePrimitives: { boolean: true, number: true, string: true }, + }, + ], output: null, }, + // falsy { code: ` -enum Enum { - A = 0, - B = 1, - C = 2, -} -declare let x: Enum.A | Enum.B | undefined; +declare let x: '' | undefined; x || y; `, errors: [ @@ -2563,28 +1976,28 @@ x || y; { messageId: 'suggestNullish', output: ` -enum Enum { - A = 0, - B = 1, - C = 2, -} -declare let x: Enum.A | Enum.B | undefined; +declare let x: '' | undefined; x ?? y; `, }, ], }, ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], output: null, }, { code: ` -enum Enum { - A = 'a', - B = 'b', - C = 'c', -} -declare let x: Enum | undefined; +declare let x: \`\` | undefined; x || y; `, errors: [ @@ -2594,28 +2007,28 @@ x || y; { messageId: 'suggestNullish', output: ` -enum Enum { - A = 'a', - B = 'b', - C = 'c', -} -declare let x: Enum | undefined; +declare let x: \`\` | undefined; x ?? y; `, }, ], }, ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], output: null, }, { code: ` -enum Enum { - A = 'a', - B = 'b', - C = 'c', -} -declare let x: Enum.A | Enum.B | undefined; +declare let x: 0 | undefined; x || y; `, errors: [ @@ -2625,27 +2038,29 @@ x || y; { messageId: 'suggestNullish', output: ` -enum Enum { - A = 'a', - B = 'b', - C = 'c', -} -declare let x: Enum.A | Enum.B | undefined; +declare let x: 0 | undefined; x ?? y; `, }, ], }, ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], output: null, }, { code: ` -let a: string | true | undefined; -let b: string | boolean | undefined; -let c: boolean | undefined; - -const x = Boolean(a || b); +declare let x: 0n | undefined; +x || y; `, errors: [ { @@ -2654,11 +2069,8 @@ const x = Boolean(a || b); { messageId: 'suggestNullish', output: ` -let a: string | true | undefined; -let b: string | boolean | undefined; -let c: boolean | undefined; - -const x = Boolean(a ?? b); +declare let x: 0n | undefined; +x ?? y; `, }, ], @@ -2666,16 +2078,20 @@ const x = Boolean(a ?? b); ], options: [ { - ignoreBooleanCoercion: false, + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, }, ], + output: null, }, { code: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -const x = String(a || b); +declare let x: false | undefined; +x || y; `, errors: [ { @@ -2684,10 +2100,8 @@ const x = String(a || b); { messageId: 'suggestNullish', output: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -const x = String(a ?? b); +declare let x: false | undefined; +x ?? y; `, }, ], @@ -2695,16 +2109,21 @@ const x = String(a ?? b); ], options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, }, ], + output: null, }, + // truthy { code: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -const x = Boolean(() => a || b); +declare let x: 'a' | undefined; +x || y; `, errors: [ { @@ -2713,10 +2132,8 @@ const x = Boolean(() => a || b); { messageId: 'suggestNullish', output: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -const x = Boolean(() => a ?? b); +declare let x: 'a' | undefined; +x ?? y; `, }, ], @@ -2724,18 +2141,20 @@ const x = Boolean(() => a ?? b); ], options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, }, ], + output: null, }, { code: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -const x = Boolean(function weird() { - return a || b; -}); +declare let x: \`hello\${'string'}\` | undefined; +x || y; `, errors: [ { @@ -2744,12 +2163,8 @@ const x = Boolean(function weird() { { messageId: 'suggestNullish', output: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -const x = Boolean(function weird() { - return a ?? b; -}); +declare let x: \`hello\${'string'}\` | undefined; +x ?? y; `, }, ], @@ -2757,18 +2172,20 @@ const x = Boolean(function weird() { ], options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, }, ], + output: null, }, { code: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -declare function f(x: unknown): unknown; - -const x = Boolean(f(a || b)); +declare let x: 1 | undefined; +x || y; `, errors: [ { @@ -2777,12 +2194,8 @@ const x = Boolean(f(a || b)); { messageId: 'suggestNullish', output: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -declare function f(x: unknown): unknown; - -const x = Boolean(f(a ?? b)); +declare let x: 1 | undefined; +x ?? y; `, }, ], @@ -2790,16 +2203,20 @@ const x = Boolean(f(a ?? b)); ], options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, }, ], + output: null, }, { code: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -const x = Boolean(1 + (a || b)); +declare let x: 1n | undefined; +x || y; `, errors: [ { @@ -2808,10 +2225,8 @@ const x = Boolean(1 + (a || b)); { messageId: 'suggestNullish', output: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -const x = Boolean(1 + (a ?? b)); +declare let x: 1n | undefined; +x ?? y; `, }, ], @@ -2819,19 +2234,20 @@ const x = Boolean(1 + (a ?? b)); ], options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, }, ], + output: null, }, { code: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -declare function f(x: unknown): unknown; - -if (f(a || b)) { -} +declare let x: true | undefined; +x || y; `, errors: [ { @@ -2840,13 +2256,8 @@ if (f(a || b)) { { messageId: 'suggestNullish', output: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -declare function f(x: unknown): unknown; - -if (f(a ?? b)) { -} +declare let x: true | undefined; +x ?? y; `, }, ], @@ -2854,17 +2265,21 @@ if (f(a ?? b)) { ], options: [ { - ignoreConditionalTests: true, + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, }, ], + output: null, }, + // Unions of same primitive { code: ` -declare const a: string | undefined; -declare const b: string; - -if (+(a || b)) { -} +declare let x: 'a' | 'b' | undefined; +x || y; `, errors: [ { @@ -2873,11 +2288,8 @@ if (+(a || b)) { { messageId: 'suggestNullish', output: ` -declare const a: string | undefined; -declare const b: string; - -if (+(a ?? b)) { -} +declare let x: 'a' | 'b' | undefined; +x ?? y; `, }, ], @@ -2885,633 +2297,718 @@ if (+(a ?? b)) { ], options: [ { - ignoreConditionalTests: true, + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, }, ], + output: null, }, { code: ` -declare const x: { n: object | null }; -declare const y: any; - -x.n ? x.n : y; +declare let x: 'a' | \`b\` | undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: object | null }; -declare const y: any; - -x.n ?? y; +declare let x: 'a' | \`b\` | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], output: null, }, { code: ` -declare const x: { n: object | null }; -declare const y: any; - -!x.n ? y : x.n; +declare let x: 0 | 1 | undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: object | null }; -declare const y: any; - -x.n ?? y; +declare let x: 0 | 1 | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], output: null, }, { code: ` -declare const x: { n: object | undefined }; -declare const y: any; - -x.n ? x.n : y; +declare let x: 1 | 2 | 3 | undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: object | undefined }; -declare const y: any; - -x.n ?? y; +declare let x: 1 | 2 | 3 | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], output: null, }, { code: ` -declare const x: { n: object | undefined }; -declare const y: any; - -!x.n ? y : x.n; +declare let x: 0n | 1n | undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: object | undefined }; -declare const y: any; - -x.n ?? y; +declare let x: 0n | 1n | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, + }, + ], output: null, }, { code: ` -declare const x: { n: object | null | undefined }; -declare const y: any; - -x.n ? x.n : y; +declare let x: 1n | 2n | 3n | undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: object | null | undefined }; -declare const y: any; - -x.n ?? y; +declare let x: 1n | 2n | 3n | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, + }, + ], output: null, }, { code: ` -declare const x: { n: object | null | undefined }; -declare const y: any; - -!x.n ? y : x.n; +declare let x: true | false | undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: object | null | undefined }; -declare const y: any; - -x.n ?? y; +declare let x: true | false | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], output: null, }, + // Mixed unions { code: ` -declare const x: { n: string[] | null }; -declare const y: any; - -x.n ? x.n : y; +declare let x: 0 | 1 | 0n | 1n | undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: string[] | null }; -declare const y: any; - -x.n ?? y; +declare let x: 0 | 1 | 0n | 1n | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: false, + string: true, + }, + }, + ], output: null, }, { code: ` -declare const x: { n: string[] | null }; -declare const y: any; - -!x.n ? y : x.n; +declare let x: true | false | null | undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: string[] | null }; -declare const y: any; - -x.n ?? y; +declare let x: true | false | null | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], output: null, }, { code: ` -declare const x: { n: string[] | undefined }; -declare const y: any; - -x.n ? x.n : y; +declare let x: null; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: string[] | undefined }; -declare const y: any; - -x.n ?? y; +declare let x: null; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], output: null, }, { code: ` -declare const x: { n: string[] | undefined }; -declare const y: any; - -!x.n ? y : x.n; +const x = undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: string[] | undefined }; -declare const y: any; - -x.n ?? y; +const x = undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], output: null, }, { code: ` -declare const x: { n: string[] | null | undefined }; -declare const y: any; - -x.n ? x.n : y; +null || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: string[] | null | undefined }; -declare const y: any; - -x.n ?? y; +null ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], output: null, }, { code: ` -declare const x: { n: string[] | null | undefined }; -declare const y: any; - -!x.n ? y : x.n; +undefined || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: string[] | null | undefined }; -declare const y: any; - -x.n ?? y; +undefined ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], output: null, }, { code: ` -declare const x: { n: Function | null }; -declare const y: any; - -x.n ? x.n : y; +enum Enum { + A = 0, + B = 1, + C = 2, +} +declare let x: Enum | undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: Function | null }; -declare const y: any; - -x.n ?? y; +enum Enum { + A = 0, + B = 1, + C = 2, +} +declare let x: Enum | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], output: null, }, { code: ` -declare const x: { n: Function | null }; -declare const y: any; - -!x.n ? y : x.n; +enum Enum { + A = 0, + B = 1, + C = 2, +} +declare let x: Enum.A | Enum.B | undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: Function | null }; -declare const y: any; - -x.n ?? y; +enum Enum { + A = 0, + B = 1, + C = 2, +} +declare let x: Enum.A | Enum.B | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], output: null, }, { code: ` -declare const x: { n: Function | undefined }; -declare const y: any; - -x.n ? x.n : y; +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare let x: Enum | undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: Function | undefined }; -declare const y: any; - -x.n ?? y; +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare let x: Enum | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], output: null, }, { code: ` -declare const x: { n: Function | undefined }; -declare const y: any; - -!x.n ? y : x.n; +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare let x: Enum.A | Enum.B | undefined; +x || y; `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: Function | undefined }; -declare const y: any; - -x.n ?? y; +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare let x: Enum.A | Enum.B | undefined; +x ?? y; `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], output: null, }, { code: ` -declare const x: { n: Function | null | undefined }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; +let c: boolean | undefined; -x.n ? x.n : y; +const x = Boolean(a || b); `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: Function | null | undefined }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; +let c: boolean | undefined; -x.n ?? y; +const x = Boolean(a ?? b); `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], - output: null, + options: [ + { + ignoreBooleanCoercion: false, + }, + ], }, { code: ` -declare const x: { n: Function | null | undefined }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; -!x.n ? y : x.n; +const x = String(a || b); `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: Function | null | undefined }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; -x.n ?? y; +const x = String(a ?? b); `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], - output: null, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], }, { code: ` -declare const x: { n: (() => string) | null }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; -x.n ? x.n : y; +const x = Boolean(() => a || b); `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: (() => string) | null }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; -x.n ?? y; +const x = Boolean(() => a ?? b); `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], - output: null, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], }, { code: ` -declare const x: { n: (() => string) | null }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; -!x.n ? y : x.n; +const x = Boolean(function weird() { + return a || b; +}); `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: (() => string) | null }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; -x.n ?? y; +const x = Boolean(function weird() { + return a ?? b; +}); `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], - output: null, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], }, { code: ` -declare const x: { n: (() => string) | undefined }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; -x.n ? x.n : y; +declare function f(x: unknown): unknown; + +const x = Boolean(f(a || b)); `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: (() => string) | undefined }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; -x.n ?? y; +declare function f(x: unknown): unknown; + +const x = Boolean(f(a ?? b)); `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], - output: null, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], }, { code: ` -declare const x: { n: (() => string) | undefined }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; -!x.n ? y : x.n; +const x = Boolean(1 + (a || b)); `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: (() => string) | undefined }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; -x.n ?? y; +const x = Boolean(1 + (a ?? b)); `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], - output: null, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], }, { code: ` -declare const x: { n: (() => string) | null | undefined }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; -x.n ? x.n : y; +declare function f(x: unknown): unknown; + +if (f(a || b)) { +} `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: (() => string) | null | undefined }; -declare const y: any; +let a: string | true | undefined; +let b: string | boolean | undefined; -x.n ?? y; +declare function f(x: unknown): unknown; + +if (f(a ?? b)) { +} `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], - output: null, + options: [ + { + ignoreConditionalTests: true, + }, + ], }, { code: ` -declare const x: { n: (() => string) | null | undefined }; -declare const y: any; +declare const a: string | undefined; +declare const b: string; -!x.n ? y : x.n; +if (+(a || b)) { +} `, errors: [ { - messageId: 'preferNullishOverTernary', + messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: { n: (() => string) | null | undefined }; -declare const y: any; +declare const a: string | undefined; +declare const b: string; -x.n ?? y; +if (+(a ?? b)) { +} `, }, ], }, ], - options: [{ ignoreTernaryTests: false }], - output: null, + options: [ + { + ignoreConditionalTests: true, + }, + ], }, { code: ` From c2e9ab0dd54a58a1ed02d01bcc3d86318b710a1f Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 19 Jan 2025 22:16:20 +0100 Subject: [PATCH 23/33] renaming --- .../src/rules/prefer-nullish-coalescing.ts | 156 ++++++++++-------- 1 file changed, 85 insertions(+), 71 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index f561d146d123..dd78ca963f0e 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -21,41 +21,6 @@ import { const isIdentifierOrMemberExpressionType = (type: TSESTree.AST_NODE_TYPES) => [AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression].includes(type); -function ignore( - type: ts.Type, - ignorePrimitives: Options[0]['ignorePrimitives'], -): boolean { - if (!isPossiblyNullish(type)) { - return true; - } - const ignorableFlags = [ - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - (ignorePrimitives === true || ignorePrimitives!.bigint) && - ts.TypeFlags.BigIntLike, - (ignorePrimitives === true || ignorePrimitives!.boolean) && - ts.TypeFlags.BooleanLike, - (ignorePrimitives === true || ignorePrimitives!.number) && - ts.TypeFlags.NumberLike, - (ignorePrimitives === true || ignorePrimitives!.string) && - ts.TypeFlags.StringLike, - /* eslint-enable @typescript-eslint/no-non-null-assertion */ - ] - .filter((flag): flag is number => typeof flag === 'number') - .reduce((previous, flag) => previous | flag, 0); - if ( - type.flags !== ts.TypeFlags.Null && - type.flags !== ts.TypeFlags.Undefined && - (type as ts.UnionOrIntersectionType).types.some(t => - tsutils - .intersectionTypeParts(t) - .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)), - ) - ) { - return true; - } - return false; -} - export type Options = [ { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; @@ -217,18 +182,51 @@ export default createRule({ }); } - // todo: rename to something more specific? - function checkAssignmentOrLogicalExpression( + function isNotPossiblyNullishOrIgnorePrimitive( + node: TSESTree.Node, + ignorePrimitives: Options[0]['ignorePrimitives'], + ): boolean { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + const type = checker.getTypeAtLocation(tsNode); + + if (!isPossiblyNullish(type)) { + return true; + } + + const ignorableFlags = [ + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + (ignorePrimitives === true || ignorePrimitives!.bigint) && + ts.TypeFlags.BigIntLike, + (ignorePrimitives === true || ignorePrimitives!.boolean) && + ts.TypeFlags.BooleanLike, + (ignorePrimitives === true || ignorePrimitives!.number) && + ts.TypeFlags.NumberLike, + (ignorePrimitives === true || ignorePrimitives!.string) && + ts.TypeFlags.StringLike, + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + ] + .filter((flag): flag is number => typeof flag === 'number') + .reduce((previous, flag) => previous | flag, 0); + if ( + type.flags !== ts.TypeFlags.Null && + type.flags !== ts.TypeFlags.Undefined && + (type as ts.UnionOrIntersectionType).types.some(t => + tsutils + .intersectionTypeParts(t) + .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)), + ) + ) { + return true; + } + + return false; + } + + function checkAndFixWithPreferNullishOverOr( node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression, description: string, equals: string, ): void { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); - const type = checker.getTypeAtLocation(tsNode.left); - if (ignore(type, ignorePrimitives)) { - return; - } - if (ignoreConditionalTests === true && isConditionalTest(node)) { return; } @@ -289,7 +287,13 @@ export default createRule({ 'AssignmentExpression[operator = "||="]'( node: TSESTree.AssignmentExpression, ): void { - checkAssignmentOrLogicalExpression(node, 'assignment', '='); + if ( + isNotPossiblyNullishOrIgnorePrimitive(node.left, ignorePrimitives) + ) { + return; + } + + checkAndFixWithPreferNullishOverOr(node, 'assignment', '='); }, ConditionalExpression(node: TSESTree.ConditionalExpression): void { if (ignoreTernaryTests) { @@ -354,70 +358,74 @@ export default createRule({ } } - let identifierOrMemberExpresion: TSESTree.Node | undefined; - let hasUndefinedCheck = false; - let hasNullCheck = false; + let identifierOrMemberExpressionNode: TSESTree.Node | undefined; let hasTruthinessCheck = false; + let hasNullCheckWithoutTruthinessCheck = false; + let hasUndefinedCheckWithoutTruthinessCheck = false; if (!operator) { - hasUndefinedCheck = true; - hasNullCheck = true; hasTruthinessCheck = true; if ( isIdentifierOrMemberExpressionType(node.test.type) && isNodeEqual(node.test, node.consequent) ) { - identifierOrMemberExpresion = node.test; + identifierOrMemberExpressionNode = node.test; } else if ( node.test.type === AST_NODE_TYPES.UnaryExpression && node.test.operator === '!' && isIdentifierOrMemberExpressionType(node.test.argument.type) && isNodeEqual(node.test.argument, node.alternate) ) { - identifierOrMemberExpresion = node.test.argument; + identifierOrMemberExpressionNode = node.test.argument; operator = '!'; } } else { // we check that the test only contains null, undefined and the identifier for (const testNode of nodesInsideTestExpression) { if (isNullLiteral(testNode)) { - hasNullCheck = true; + hasNullCheckWithoutTruthinessCheck = true; } else if (isUndefinedIdentifier(testNode)) { - hasUndefinedCheck = true; + hasUndefinedCheckWithoutTruthinessCheck = true; } else if ( (operator === '!==' || operator === '!=') && isNodeEqual(testNode, node.consequent) ) { - identifierOrMemberExpresion = testNode; + identifierOrMemberExpressionNode = testNode; } else if ( (operator === '===' || operator === '==') && isNodeEqual(testNode, node.alternate) ) { - identifierOrMemberExpresion = testNode; + identifierOrMemberExpressionNode = testNode; } } } - if (!identifierOrMemberExpresion) { + if (!identifierOrMemberExpressionNode) { return; } - const isFixable = ((): boolean => { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get( - identifierOrMemberExpresion, - ); - const type = checker.getTypeAtLocation(tsNode); - + const isFixableWithPreferNullishOverTernary = ((): boolean => { // x ? x : y and !x ? y : x patterns if (hasTruthinessCheck) { - return !ignore(type, ignorePrimitives); + return !isNotPossiblyNullishOrIgnorePrimitive( + identifierOrMemberExpressionNode, + ignorePrimitives, + ); } + const tsNode = parserServices.esTreeNodeToTSNodeMap.get( + identifierOrMemberExpressionNode, + ); + const type = checker.getTypeAtLocation(tsNode); + const flags = getTypeFlags(type); // it is fixable if we check for both null and undefined, or not if neither - if (hasUndefinedCheck === hasNullCheck) { - return hasUndefinedCheck; + if ( + hasUndefinedCheckWithoutTruthinessCheck === + hasNullCheckWithoutTruthinessCheck + ) { + return hasUndefinedCheckWithoutTruthinessCheck; } // it is fixable if we loosely check for either null or undefined @@ -432,17 +440,17 @@ export default createRule({ const hasNullType = (flags & ts.TypeFlags.Null) !== 0; // it is fixable if we check for undefined and the type is not nullable - if (hasUndefinedCheck && !hasNullType) { + if (hasUndefinedCheckWithoutTruthinessCheck && !hasNullType) { return true; } const hasUndefinedType = (flags & ts.TypeFlags.Undefined) !== 0; // it is fixable if we check for null and the type can't be undefined - return hasNullCheck && !hasUndefinedType; + return hasNullCheckWithoutTruthinessCheck && !hasUndefinedType; })(); - if (isFixable) { + if (isFixableWithPreferNullishOverTernary) { context.report({ node, messageId: 'preferNullishOverTernary', @@ -455,8 +463,8 @@ export default createRule({ fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { const [left, right] = operator === '===' || operator === '==' || operator === '!' - ? [identifierOrMemberExpresion, node.consequent] - : [identifierOrMemberExpresion, node.alternate]; + ? [identifierOrMemberExpressionNode, node.consequent] + : [identifierOrMemberExpressionNode, node.alternate]; return fixer.replaceText( node, `${getTextWithParentheses(context.sourceCode, left)} ?? ${getTextWithParentheses( @@ -473,6 +481,12 @@ export default createRule({ 'LogicalExpression[operator = "||"]'( node: TSESTree.LogicalExpression, ): void { + if ( + isNotPossiblyNullishOrIgnorePrimitive(node.left, ignorePrimitives) + ) { + return; + } + if ( ignoreBooleanCoercion === true && isBooleanConstructorContext(node, context) @@ -480,7 +494,7 @@ export default createRule({ return; } - checkAssignmentOrLogicalExpression(node, 'or', ''); + checkAndFixWithPreferNullishOverOr(node, 'or', ''); }, }; }, From 70e5aeb5e3ac97552cc8ec7b49c149f6fcf95a4d Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sun, 19 Jan 2025 23:50:42 +0100 Subject: [PATCH 24/33] change break line --- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index dd78ca963f0e..6c82a520e9fe 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -418,8 +418,8 @@ export default createRule({ identifierOrMemberExpressionNode, ); const type = checker.getTypeAtLocation(tsNode); - const flags = getTypeFlags(type); + // it is fixable if we check for both null and undefined, or not if neither if ( hasUndefinedCheckWithoutTruthinessCheck === From 80c0221823e8628a4496397cf907e8cfa11f064a Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:20:41 +0100 Subject: [PATCH 25/33] Add tests for primitives + ternary --- .../rules/prefer-nullish-coalescing.test.ts | 1369 +++++++++++++++-- 1 file changed, 1206 insertions(+), 163 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index fed5a063b090..61cb580cb1ba 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -444,6 +444,62 @@ x || y; `, options: [{ ignorePrimitives: true }], })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: ${type} | undefined; +x ? x : y; + `, + options: [{ ignorePrimitives: { [type]: true } }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: ${type} | undefined; +!x ? y : x; + `, + options: [{ ignorePrimitives: { [type]: true } }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: ${type} | undefined; +x ? x : y; + `, + options: [{ ignorePrimitives: true }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: ${type} | undefined; +!x ? y : x; + `, + options: [{ ignorePrimitives: true }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: (${type} & { __brand?: any }) | undefined; +x ? x : y; + `, + options: [{ ignorePrimitives: { [type]: true } }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: (${type} & { __brand?: any }) | undefined; +!x ? y : x; + `, + options: [{ ignorePrimitives: { [type]: true } }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: (${type} & { __brand?: any }) | undefined; +x ? x : y; + `, + options: [{ ignorePrimitives: true }], + })), + ...ignorablePrimitiveTypes.map>(type => ({ + code: ` +declare let x: (${type} & { __brand?: any }) | undefined; +!x ? y : x; + `, + options: [{ ignorePrimitives: true }], + })), ` declare let x: any; declare let y: number; @@ -459,6 +515,36 @@ x || y; declare let y: number; x || y; `, + ` + declare let x: any; + declare let y: number; + x ? x : y; + `, + ` + declare let x: any; + declare let y: number; + !x ? y : x; + `, + ` + declare let x: unknown; + declare let y: number; + x ? x : y; + `, + ` + declare let x: unknown; + declare let y: number; + !x ? y : x; + `, + ` + declare let x: never; + declare let y: number; + x ? x : y; + `, + ` + declare let x: never; + declare let y: number; + !x ? y : x; + `, { code: ` declare let x: 0 | 1 | 0n | 1n | undefined; @@ -593,188 +679,452 @@ x || y; }, { code: ` -let a: string | true | undefined; -let b: string | boolean | undefined; - -const x = Boolean(a || b); +declare let x: 0 | 1 | 0n | 1n | undefined; +x ? x : y; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -const test = Boolean(a || b || c); +declare let x: 0 | 1 | 0n | 1n | undefined; +!x ? y : x; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -const test = Boolean(a || (b && c)); +declare let x: 0 | 1 | 0n | 1n | undefined; +x ? x : y; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -const test = Boolean((a || b) ?? c); +declare let x: 0 | 1 | 0n | 1n | undefined; +!x ? y : x; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -const test = Boolean(a ?? (b || c)); +declare let x: 0 | 'foo' | undefined; +x ? x : y; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + number: true, + string: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -const test = Boolean(a ? b || c : 'fail'); +declare let x: 0 | 'foo' | undefined; +!x ? y : x; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + number: true, + string: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -const test = Boolean(a ? 'success' : b || c); +declare let x: 0 | 'foo' | undefined; +x ? x : y; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + number: true, + string: false, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -const test = Boolean(((a = b), b || c)); +declare let x: 0 | 'foo' | undefined; +!x ? y : x; `, options: [ { - ignoreBooleanCoercion: true, + ignorePrimitives: { + number: true, + string: false, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -if (a || b || c) { +enum Enum { + A = 0, + B = 1, + C = 2, } +declare let x: Enum | undefined; +x ? x : y; `, options: [ { - ignoreConditionalTests: true, + ignorePrimitives: { + number: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -if (a || (b && c)) { +enum Enum { + A = 0, + B = 1, + C = 2, } +declare let x: Enum | undefined; +!x ? y : x; `, options: [ { - ignoreConditionalTests: true, + ignorePrimitives: { + number: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -if ((a || b) ?? c) { +enum Enum { + A = 0, + B = 1, + C = 2, } +declare let x: Enum.A | Enum.B | undefined; +x ? x : y; `, options: [ { - ignoreConditionalTests: true, + ignorePrimitives: { + number: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -if (a ?? (b || c)) { +enum Enum { + A = 0, + B = 1, + C = 2, } +declare let x: Enum.A | Enum.B | undefined; +!x ? y : x; `, options: [ { - ignoreConditionalTests: true, + ignorePrimitives: { + number: true, + }, }, ], }, { code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - -if (a ? b || c : 'fail') { +enum Enum { + A = 'a', + B = 'b', + C = 'c', } +declare let x: Enum | undefined; +x ? x : y; `, options: [ { - ignoreConditionalTests: true, - }, + ignorePrimitives: { + string: true, + }, + }, + ], + }, + { + code: ` +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare let x: Enum | undefined; +!x ? y : x; + `, + options: [ + { + ignorePrimitives: { + string: true, + }, + }, + ], + }, + { + code: ` +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare let x: Enum.A | Enum.B | undefined; +x ? x : y; + `, + options: [ + { + ignorePrimitives: { + string: true, + }, + }, + ], + }, + { + code: ` +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare let x: Enum.A | Enum.B | undefined; +!x ? y : x; + `, + options: [ + { + ignorePrimitives: { + string: true, + }, + }, + ], + }, + { + code: ` +let a: string | true | undefined; +let b: string | boolean | undefined; + +const x = Boolean(a || b); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +const test = Boolean(a || b || c); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +const test = Boolean(a || (b && c)); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +const test = Boolean((a || b) ?? c); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +const test = Boolean(a ?? (b || c)); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +const test = Boolean(a ? b || c : 'fail'); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +const test = Boolean(a ? 'success' : b || c); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +const test = Boolean(((a = b), b || c)); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if (a || b || c) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if (a || (b && c)) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if ((a || b) ?? c) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if (a ?? (b || c)) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if (a ? b || c : 'fail') { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, ], }, { @@ -1838,30 +2188,643 @@ a || b || c; `, errors: [ { - column: 3, - endColumn: 5, - endLine: 5, - line: 5, + column: 3, + endColumn: 5, + endLine: 5, + line: 5, + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type}; +declare let c: ${type}; +(a ?? b) || c; + `, + }, + ], + }, + ], + output: null, + })), + // default for missing option + { + code: ` +declare let x: string | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: string | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { bigint: true, boolean: true, number: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: number | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: number | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { bigint: true, boolean: true, string: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: boolean | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: boolean | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { bigint: true, number: true, string: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: bigint | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: bigint | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { boolean: true, number: true, string: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: string | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: string | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { bigint: true, boolean: true, number: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: number | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: number | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { bigint: true, boolean: true, string: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: boolean | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: boolean | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { bigint: true, number: true, string: true }, + }, + ], + output: null, + }, + { + code: ` +declare let x: bigint | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: bigint | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { boolean: true, number: true, string: true }, + }, + ], + output: null, + }, + // falsy + { + code: ` +declare let x: '' | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: '' | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: \`\` | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: \`\` | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 0 | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 0 | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 0n | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 0n | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: false | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: false | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: '' | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: '' | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: \`\` | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: \`\` | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 0 | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 0 | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 0n | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 0n | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: false | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: false | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], + output: null, + }, + // truthy + { + code: ` +declare let x: 'a' | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 'a' | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: \`hello\${'string'}\` | undefined; +x || y; + `, + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: \`hello\${'string'}\` | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 1 | undefined; +x || y; + `, + errors: [ + { messageId: 'preferNullishOverOr', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let a: ${type} | ${nullish}; -declare let b: ${type}; -declare let c: ${type}; -(a ?? b) || c; +declare let x: 1 | undefined; +x ?? y; `, }, ], }, ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], output: null, - })), - // default for missing option + }, { code: ` -declare let x: string | undefined; +declare let x: 1n | undefined; x || y; `, errors: [ @@ -1871,7 +2834,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: string | undefined; +declare let x: 1n | undefined; x ?? y; `, }, @@ -1880,14 +2843,19 @@ x ?? y; ], options: [ { - ignorePrimitives: { bigint: true, boolean: true, number: true }, + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, }, ], output: null, }, { code: ` -declare let x: number | undefined; +declare let x: true | undefined; x || y; `, errors: [ @@ -1897,7 +2865,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: number | undefined; +declare let x: true | undefined; x ?? y; `, }, @@ -1906,24 +2874,29 @@ x ?? y; ], options: [ { - ignorePrimitives: { bigint: true, boolean: true, string: true }, + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, }, ], output: null, }, { code: ` -declare let x: boolean | undefined; -x || y; +declare let x: 'a' | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: boolean | undefined; +declare let x: 'a' | undefined; x ?? y; `, }, @@ -1932,24 +2905,29 @@ x ?? y; ], options: [ { - ignorePrimitives: { bigint: true, number: true, string: true }, + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, }, ], output: null, }, { code: ` -declare let x: bigint | undefined; -x || y; +declare let x: \`hello\${'string'}\` | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: bigint | undefined; +declare let x: \`hello\${'string'}\` | undefined; x ?? y; `, }, @@ -1958,25 +2936,29 @@ x ?? y; ], options: [ { - ignorePrimitives: { boolean: true, number: true, string: true }, + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, }, ], output: null, }, - // falsy { code: ` -declare let x: '' | undefined; -x || y; +declare let x: 1 | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: '' | undefined; +declare let x: 1 | undefined; x ?? y; `, }, @@ -1988,8 +2970,8 @@ x ?? y; ignorePrimitives: { bigint: true, boolean: true, - number: true, - string: false, + number: false, + string: true, }, }, ], @@ -1997,17 +2979,17 @@ x ?? y; }, { code: ` -declare let x: \`\` | undefined; -x || y; +declare let x: 1n | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: \`\` | undefined; +declare let x: 1n | undefined; x ?? y; `, }, @@ -2017,10 +2999,10 @@ x ?? y; options: [ { ignorePrimitives: { - bigint: true, + bigint: false, boolean: true, number: true, - string: false, + string: true, }, }, ], @@ -2028,17 +3010,17 @@ x ?? y; }, { code: ` -declare let x: 0 | undefined; -x || y; +declare let x: true | undefined; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', output: ` -declare let x: 0 | undefined; +declare let x: true | undefined; x ?? y; `, }, @@ -2049,17 +3031,18 @@ x ?? y; { ignorePrimitives: { bigint: true, - boolean: true, - number: false, + boolean: false, + number: true, string: true, }, }, ], output: null, }, + // Unions of same primitive { code: ` -declare let x: 0n | undefined; +declare let x: 'a' | 'b' | undefined; x || y; `, errors: [ @@ -2069,7 +3052,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: 0n | undefined; +declare let x: 'a' | 'b' | undefined; x ?? y; `, }, @@ -2079,10 +3062,10 @@ x ?? y; options: [ { ignorePrimitives: { - bigint: false, + bigint: true, boolean: true, number: true, - string: true, + string: false, }, }, ], @@ -2090,7 +3073,7 @@ x ?? y; }, { code: ` -declare let x: false | undefined; +declare let x: 'a' | \`b\` | undefined; x || y; `, errors: [ @@ -2100,7 +3083,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: false | undefined; +declare let x: 'a' | \`b\` | undefined; x ?? y; `, }, @@ -2111,18 +3094,17 @@ x ?? y; { ignorePrimitives: { bigint: true, - boolean: false, + boolean: true, number: true, - string: true, + string: false, }, }, ], output: null, }, - // truthy { code: ` -declare let x: 'a' | undefined; +declare let x: 0 | 1 | undefined; x || y; `, errors: [ @@ -2132,7 +3114,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: 'a' | undefined; +declare let x: 0 | 1 | undefined; x ?? y; `, }, @@ -2144,8 +3126,8 @@ x ?? y; ignorePrimitives: { bigint: true, boolean: true, - number: true, - string: false, + number: false, + string: true, }, }, ], @@ -2153,7 +3135,7 @@ x ?? y; }, { code: ` -declare let x: \`hello\${'string'}\` | undefined; +declare let x: 1 | 2 | 3 | undefined; x || y; `, errors: [ @@ -2163,7 +3145,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: \`hello\${'string'}\` | undefined; +declare let x: 1 | 2 | 3 | undefined; x ?? y; `, }, @@ -2175,8 +3157,8 @@ x ?? y; ignorePrimitives: { bigint: true, boolean: true, - number: true, - string: false, + number: false, + string: true, }, }, ], @@ -2184,7 +3166,7 @@ x ?? y; }, { code: ` -declare let x: 1 | undefined; +declare let x: 0n | 1n | undefined; x || y; `, errors: [ @@ -2194,7 +3176,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: 1 | undefined; +declare let x: 0n | 1n | undefined; x ?? y; `, }, @@ -2204,9 +3186,9 @@ x ?? y; options: [ { ignorePrimitives: { - bigint: true, + bigint: false, boolean: true, - number: false, + number: true, string: true, }, }, @@ -2215,7 +3197,7 @@ x ?? y; }, { code: ` -declare let x: 1n | undefined; +declare let x: 1n | 2n | 3n | undefined; x || y; `, errors: [ @@ -2225,7 +3207,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: 1n | undefined; +declare let x: 1n | 2n | 3n | undefined; x ?? y; `, }, @@ -2246,7 +3228,7 @@ x ?? y; }, { code: ` -declare let x: true | undefined; +declare let x: true | false | undefined; x || y; `, errors: [ @@ -2256,7 +3238,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare let x: true | undefined; +declare let x: true | false | undefined; x ?? y; `, }, @@ -2275,15 +3257,14 @@ x ?? y; ], output: null, }, - // Unions of same primitive { code: ` declare let x: 'a' | 'b' | undefined; -x || y; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', @@ -2310,11 +3291,11 @@ x ?? y; { code: ` declare let x: 'a' | \`b\` | undefined; -x || y; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', @@ -2341,11 +3322,11 @@ x ?? y; { code: ` declare let x: 0 | 1 | undefined; -x || y; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', @@ -2372,11 +3353,11 @@ x ?? y; { code: ` declare let x: 1 | 2 | 3 | undefined; -x || y; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', @@ -2403,11 +3384,11 @@ x ?? y; { code: ` declare let x: 0n | 1n | undefined; -x || y; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', @@ -2434,11 +3415,11 @@ x ?? y; { code: ` declare let x: 1n | 2n | 3n | undefined; -x || y; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', @@ -2465,11 +3446,11 @@ x ?? y; { code: ` declare let x: true | false | undefined; -x || y; +x ? x : y; `, errors: [ { - messageId: 'preferNullishOverOr', + messageId: 'preferNullishOverTernary', suggestions: [ { messageId: 'suggestNullish', @@ -2538,6 +3519,68 @@ x || y; messageId: 'suggestNullish', output: ` declare let x: true | false | null | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 0 | 1 | 0n | 1n | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 0 | 1 | 0n | 1n | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: false, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: true | false | null | undefined; +x ? x : y; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: true | false | null | undefined; x ?? y; `, }, From ddb404c6912ed9cf6c5787f85b1b7a6e6922b65d Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:30:40 +0100 Subject: [PATCH 26/33] Add tests for primitives + ternary --- .../rules/prefer-nullish-coalescing.test.ts | 434 ++++++++++++++++++ 1 file changed, 434 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 61cb580cb1ba..d7a82d4b1dc1 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -2897,6 +2897,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: 'a' | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 'a' | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 'a' | undefined; x ?? y; `, }, @@ -2928,6 +2959,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: \`hello\${'string'}\` | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: \`hello\${'string'}\` | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: \`hello\${'string'}\` | undefined; x ?? y; `, }, @@ -2959,6 +3021,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: 1 | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 1 | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 1 | undefined; x ?? y; `, }, @@ -2990,6 +3083,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: 1n | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 1n | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 1n | undefined; x ?? y; `, }, @@ -3021,6 +3145,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: true | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: true | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: true | undefined; x ?? y; `, }, @@ -3270,6 +3425,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: 'a' | 'b' | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 'a' | 'b' | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 'a' | 'b' | undefined; x ?? y; `, }, @@ -3301,6 +3487,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: 'a' | \`b\` | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: true, + string: false, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 'a' | \`b\` | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 'a' | \`b\` | undefined; x ?? y; `, }, @@ -3332,6 +3549,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: 0 | 1 | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 0 | 1 | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 0 | 1 | undefined; x ?? y; `, }, @@ -3363,6 +3611,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: 1 | 2 | 3 | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: true, + number: false, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 1 | 2 | 3 | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 1 | 2 | 3 | undefined; x ?? y; `, }, @@ -3394,6 +3673,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: 0n | 1n | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 0n | 1n | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 0n | 1n | undefined; x ?? y; `, }, @@ -3425,6 +3735,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: 1n | 2n | 3n | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 1n | 2n | 3n | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 1n | 2n | 3n | undefined; x ?? y; `, }, @@ -3456,6 +3797,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: true | false | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: true | false | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: true | false | undefined; x ?? y; `, }, @@ -3550,6 +3922,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: 0 | 1 | 0n | 1n | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: false, + boolean: true, + number: false, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: 0 | 1 | 0n | 1n | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: 0 | 1 | 0n | 1n | undefined; x ?? y; `, }, @@ -3581,6 +3984,37 @@ x ? x : y; messageId: 'suggestNullish', output: ` declare let x: true | false | null | undefined; +x ?? y; + `, + }, + ], + }, + ], + options: [ + { + ignorePrimitives: { + bigint: true, + boolean: false, + number: true, + string: true, + }, + }, + ], + output: null, + }, + { + code: ` +declare let x: true | false | null | undefined; +!x ? y : x; + `, + errors: [ + { + messageId: 'preferNullishOverTernary', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare let x: true | false | null | undefined; x ?? y; `, }, From 2cfb35cd9852cfebf69d2deed7157695a992b3f6 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 21 Jan 2025 10:02:20 +0100 Subject: [PATCH 27/33] align options for as much use cases as possible --- .../src/rules/prefer-nullish-coalescing.ts | 78 +++++++++++-------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 6c82a520e9fe..aede7a8217e3 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -21,6 +21,14 @@ import { const isIdentifierOrMemberExpressionType = (type: TSESTree.AST_NODE_TYPES) => [AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression].includes(type); +const isAssignmentOrLogicalExpression = ( + node: TSESTree.Node, +): node is TSESTree.AssignmentExpression | TSESTree.LogicalExpression => + [ + AST_NODE_TYPES.AssignmentExpression, + AST_NODE_TYPES.LogicalExpression, + ].includes(node.type); + export type Options = [ { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; @@ -222,12 +230,35 @@ export default createRule({ return false; } + function isEligibleForPreferNullish( + node: TSESTree.Node, + ignorePrimitives: Options[0]['ignorePrimitives'], + ): boolean { + const mainNode = isAssignmentOrLogicalExpression(node) ? node.left : node; + if (isNotPossiblyNullishOrIgnorePrimitive(mainNode, ignorePrimitives)) { + return false; + } + + if (ignoreConditionalTests === true && isConditionalTest(node)) { + return false; + } + + if ( + ignoreBooleanCoercion === true && + isBooleanConstructorContext(node, context) + ) { + return false; + } + + return true; + } + function checkAndFixWithPreferNullishOverOr( node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression, description: string, equals: string, ): void { - if (ignoreConditionalTests === true && isConditionalTest(node)) { + if (!isEligibleForPreferNullish(node, ignorePrimitives)) { return; } @@ -287,12 +318,6 @@ export default createRule({ 'AssignmentExpression[operator = "||="]'( node: TSESTree.AssignmentExpression, ): void { - if ( - isNotPossiblyNullishOrIgnorePrimitive(node.left, ignorePrimitives) - ) { - return; - } - checkAndFixWithPreferNullishOverOr(node, 'assignment', '='); }, ConditionalExpression(node: TSESTree.ConditionalExpression): void { @@ -358,7 +383,7 @@ export default createRule({ } } - let identifierOrMemberExpressionNode: TSESTree.Node | undefined; + let identifierOrMemberExpression: TSESTree.Node | undefined; let hasTruthinessCheck = false; let hasNullCheckWithoutTruthinessCheck = false; let hasUndefinedCheckWithoutTruthinessCheck = false; @@ -370,14 +395,14 @@ export default createRule({ isIdentifierOrMemberExpressionType(node.test.type) && isNodeEqual(node.test, node.consequent) ) { - identifierOrMemberExpressionNode = node.test; + identifierOrMemberExpression = node.test; } else if ( node.test.type === AST_NODE_TYPES.UnaryExpression && node.test.operator === '!' && isIdentifierOrMemberExpressionType(node.test.argument.type) && isNodeEqual(node.test.argument, node.alternate) ) { - identifierOrMemberExpressionNode = node.test.argument; + identifierOrMemberExpression = node.test.argument; operator = '!'; } } else { @@ -391,31 +416,31 @@ export default createRule({ (operator === '!==' || operator === '!=') && isNodeEqual(testNode, node.consequent) ) { - identifierOrMemberExpressionNode = testNode; + identifierOrMemberExpression = testNode; } else if ( (operator === '===' || operator === '==') && isNodeEqual(testNode, node.alternate) ) { - identifierOrMemberExpressionNode = testNode; + identifierOrMemberExpression = testNode; } } } - if (!identifierOrMemberExpressionNode) { + if (!identifierOrMemberExpression) { return; } const isFixableWithPreferNullishOverTernary = ((): boolean => { // x ? x : y and !x ? y : x patterns if (hasTruthinessCheck) { - return !isNotPossiblyNullishOrIgnorePrimitive( - identifierOrMemberExpressionNode, + return isEligibleForPreferNullish( + identifierOrMemberExpression, ignorePrimitives, ); } const tsNode = parserServices.esTreeNodeToTSNodeMap.get( - identifierOrMemberExpressionNode, + identifierOrMemberExpression, ); const type = checker.getTypeAtLocation(tsNode); const flags = getTypeFlags(type); @@ -463,8 +488,8 @@ export default createRule({ fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { const [left, right] = operator === '===' || operator === '==' || operator === '!' - ? [identifierOrMemberExpressionNode, node.consequent] - : [identifierOrMemberExpressionNode, node.alternate]; + ? [identifierOrMemberExpression, node.consequent] + : [identifierOrMemberExpression, node.alternate]; return fixer.replaceText( node, `${getTextWithParentheses(context.sourceCode, left)} ?? ${getTextWithParentheses( @@ -481,19 +506,6 @@ export default createRule({ 'LogicalExpression[operator = "||"]'( node: TSESTree.LogicalExpression, ): void { - if ( - isNotPossiblyNullishOrIgnorePrimitive(node.left, ignorePrimitives) - ) { - return; - } - - if ( - ignoreBooleanCoercion === true && - isBooleanConstructorContext(node, context) - ) { - return; - } - checkAndFixWithPreferNullishOverOr(node, 'or', ''); }, }; @@ -501,6 +513,10 @@ export default createRule({ }); function isConditionalTest(node: TSESTree.Node): boolean { + if (isIdentifierOrMemberExpressionType(node.type)) { + return false; + } + const parent = node.parent; if (parent == null) { return false; From 993d6b1e7c29762d13d83c84af10c05f262a716f Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 21 Jan 2025 10:57:58 +0100 Subject: [PATCH 28/33] add tests for ternary + bool coercion --- .../src/rules/prefer-nullish-coalescing.ts | 9 ++- .../rules/prefer-nullish-coalescing.test.ts | 69 +++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index aede7a8217e3..454dc95967da 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -570,13 +570,18 @@ function isBooleanConstructorContext( return false; } - if (parent.type === AST_NODE_TYPES.LogicalExpression) { + if ( + parent.type === AST_NODE_TYPES.LogicalExpression || + parent.type === AST_NODE_TYPES.UnaryExpression + ) { return isBooleanConstructorContext(parent, context); } if ( parent.type === AST_NODE_TYPES.ConditionalExpression && - (parent.consequent === node || parent.alternate === node) + (parent.test === node || + parent.consequent === node || + parent.alternate === node) ) { return isBooleanConstructorContext(parent, context); } diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index d7a82d4b1dc1..26decf113201 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -1054,6 +1054,75 @@ const test = Boolean(((a = b), b || c)); }, { code: ` +let a: string | true | undefined; +let b: string | boolean | undefined; + +const x = Boolean(a ? a : b); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +const test = Boolean(!a ? b : a); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +const test = Boolean((a ? a : b) || c); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +const test = Boolean(c || (!a ? b : a)); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +const test = Boolean(!a ? b || c : a); + `, + options: [ + { + ignoreBooleanCoercion: true, + }, + ], + }, + { + code: ` let a: string | boolean | undefined; let b: string | boolean | undefined; let c: string | boolean | undefined; From 4822bb43b2854d3940175e90393f5e61e095562b Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:01:00 +0100 Subject: [PATCH 29/33] fixes and add test ternary + conditional --- .../src/rules/prefer-nullish-coalescing.ts | 27 ++++--- .../rules/prefer-nullish-coalescing.test.ts | 73 +++++++++++++++---- 2 files changed, 73 insertions(+), 27 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 454dc95967da..12682610d710 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -513,22 +513,24 @@ export default createRule({ }); function isConditionalTest(node: TSESTree.Node): boolean { - if (isIdentifierOrMemberExpressionType(node.type)) { - return false; - } - const parent = node.parent; if (parent == null) { return false; } + if (isIdentifierOrMemberExpressionType(node.type)) { + return isConditionalTest(parent); + } + if (parent.type === AST_NODE_TYPES.LogicalExpression) { return isConditionalTest(parent); } if ( parent.type === AST_NODE_TYPES.ConditionalExpression && - (parent.consequent === node || parent.alternate === node) + (parent.consequent === node || + parent.alternate === node || + node.type === AST_NODE_TYPES.UnaryExpression) ) { return isConditionalTest(parent); } @@ -570,18 +572,19 @@ function isBooleanConstructorContext( return false; } - if ( - parent.type === AST_NODE_TYPES.LogicalExpression || - parent.type === AST_NODE_TYPES.UnaryExpression - ) { + if (isIdentifierOrMemberExpressionType(node.type)) { + return isBooleanConstructorContext(parent, context); + } + + if (parent.type === AST_NODE_TYPES.LogicalExpression) { return isBooleanConstructorContext(parent, context); } if ( parent.type === AST_NODE_TYPES.ConditionalExpression && - (parent.test === node || - parent.consequent === node || - parent.alternate === node) + (parent.consequent === node || + parent.alternate === node || + node.type === AST_NODE_TYPES.UnaryExpression) ) { return isBooleanConstructorContext(parent, context); } diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 26decf113201..51442dbc7467 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -1069,7 +1069,6 @@ const x = Boolean(a ? a : b); code: ` let a: string | boolean | undefined; let b: string | boolean | undefined; -let c: string | boolean | undefined; const test = Boolean(!a ? b : a); `, @@ -1113,20 +1112,6 @@ let a: string | boolean | undefined; let b: string | boolean | undefined; let c: string | boolean | undefined; -const test = Boolean(!a ? b || c : a); - `, - options: [ - { - ignoreBooleanCoercion: true, - }, - ], - }, - { - code: ` -let a: string | boolean | undefined; -let b: string | boolean | undefined; -let c: string | boolean | undefined; - if (a || b || c) { } `, @@ -1246,6 +1231,64 @@ let a: string | undefined; let b: string | undefined; if (!!(a || b)) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | true | undefined; +let b: string | boolean | undefined; + +if (a ? a : b) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; + +if (!a ? b : a) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if ((a ? a : b) || c) { +} + `, + options: [ + { + ignoreConditionalTests: true, + }, + ], + }, + { + code: ` +let a: string | boolean | undefined; +let b: string | boolean | undefined; +let c: string | boolean | undefined; + +if (c || (!a ? b : a)) { } `, options: [ From 2defdf47580389a82f6e751ee30bffa1dc85f832 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:50:15 -0800 Subject: [PATCH 30/33] suggestion --- .cspell.json | 5 +- .../src/rules/prefer-nullish-coalescing.ts | 70 +++++++++++-------- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/.cspell.json b/.cspell.json index 57220ca39e05..6838353a1e50 100644 --- a/.cspell.json +++ b/.cspell.json @@ -140,6 +140,7 @@ "noninteractive", "Nrwl", "nullish", + "nullishness", "nx", "nx's", "onboarded", @@ -166,11 +167,11 @@ "redeclared", "reimplement", "resync", - "ronami", - "Ronen", "Ribaudo", "ROADMAP", "Romain", + "ronami", + "Ronen", "Rosenwasser", "ruleset", "rulesets", diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 12682610d710..280acef433b0 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -190,15 +190,13 @@ export default createRule({ }); } - function isNotPossiblyNullishOrIgnorePrimitive( - node: TSESTree.Node, - ignorePrimitives: Options[0]['ignorePrimitives'], - ): boolean { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); - const type = checker.getTypeAtLocation(tsNode); - + /** + * Checks whether a type tested for truthiness is eligible for conversion to + * a nullishness check, taking into account the rule's configuration. + */ + function isTypeEligibleForPreferNullish(type: ts.Type): boolean { if (!isPossiblyNullish(type)) { - return true; + return false; } const ignorableFlags = [ @@ -224,18 +222,35 @@ export default createRule({ .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)), ) ) { - return true; + return false; } - return false; + return true; } - function isEligibleForPreferNullish( - node: TSESTree.Node, - ignorePrimitives: Options[0]['ignorePrimitives'], - ): boolean { - const mainNode = isAssignmentOrLogicalExpression(node) ? node.left : node; - if (isNotPossiblyNullishOrIgnorePrimitive(mainNode, ignorePrimitives)) { + /** + * Determines whether a control flow construct that uses the truthiness of + * a test expression is eligible for conversion to the nullish coalescing + * operator, taking into account (both dependent on the rule's configuration): + * 1. Whether the construct is in a permitted syntactic context + * 2. Whether the type of the test expression is deemed eligible for + * conversion + * + * @param node The overall node to be converted (e.g. `a || b` or `a ? a : b`) + * @param testNode The node being tested (i.e. `a`) + */ + function isTruthinessCheckEligibleForPreferNullish({ + node, + testNode, + }: { + node: + | TSESTree.AssignmentExpression + | TSESTree.ConditionalExpression + | TSESTree.LogicalExpression; + testNode: TSESTree.Node; + }): boolean { + const testType = parserServices.getTypeAtLocation(testNode); + if (!isTypeEligibleForPreferNullish(testType)) { return false; } @@ -258,7 +273,12 @@ export default createRule({ description: string, equals: string, ): void { - if (!isEligibleForPreferNullish(node, ignorePrimitives)) { + if ( + !isTruthinessCheckEligibleForPreferNullish({ + node, + testNode: node.left, + }) + ) { return; } @@ -433,10 +453,10 @@ export default createRule({ const isFixableWithPreferNullishOverTernary = ((): boolean => { // x ? x : y and !x ? y : x patterns if (hasTruthinessCheck) { - return isEligibleForPreferNullish( - identifierOrMemberExpression, - ignorePrimitives, - ); + return isTruthinessCheckEligibleForPreferNullish({ + node, + testNode: identifierOrMemberExpression, + }); } const tsNode = parserServices.esTreeNodeToTSNodeMap.get( @@ -518,10 +538,6 @@ function isConditionalTest(node: TSESTree.Node): boolean { return false; } - if (isIdentifierOrMemberExpressionType(node.type)) { - return isConditionalTest(parent); - } - if (parent.type === AST_NODE_TYPES.LogicalExpression) { return isConditionalTest(parent); } @@ -572,10 +588,6 @@ function isBooleanConstructorContext( return false; } - if (isIdentifierOrMemberExpressionType(node.type)) { - return isBooleanConstructorContext(parent, context); - } - if (parent.type === AST_NODE_TYPES.LogicalExpression) { return isBooleanConstructorContext(parent, context); } From 80f3a2015669d8e09a98a056dd203d8579ce8655 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:42:52 +0100 Subject: [PATCH 31/33] fix lint --- .../eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 280acef433b0..f7a4361b6147 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -21,14 +21,6 @@ import { const isIdentifierOrMemberExpressionType = (type: TSESTree.AST_NODE_TYPES) => [AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression].includes(type); -const isAssignmentOrLogicalExpression = ( - node: TSESTree.Node, -): node is TSESTree.AssignmentExpression | TSESTree.LogicalExpression => - [ - AST_NODE_TYPES.AssignmentExpression, - AST_NODE_TYPES.LogicalExpression, - ].includes(node.type); - export type Options = [ { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; From 469bd65811ba813c08b87ac4179754607a1641c3 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 22 Jan 2025 01:03:21 +0100 Subject: [PATCH 32/33] Update packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts Co-authored-by: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> --- .../eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index f7a4361b6147..815415be614d 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -18,8 +18,10 @@ import { NullThrowsReasons, } from '../util'; -const isIdentifierOrMemberExpressionType = (type: TSESTree.AST_NODE_TYPES) => - [AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression].includes(type); +const isIdentifierOrMemberExpression = isNodeOfTypes([ + AST_NODE_TYPES.Identifier, + AST_NODE_TYPES.MemberExpression, +] as const); export type Options = [ { From b771c7f1c0c10095ebb060c35dc66d9400e6a05b Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Wed, 22 Jan 2025 01:18:10 +0100 Subject: [PATCH 33/33] nits --- .../docs/rules/prefer-nullish-coalescing.mdx | 1 + .../src/rules/prefer-nullish-coalescing.ts | 13 +++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx index 5e566e2e49b8..be44bfe11d88 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx @@ -16,6 +16,7 @@ This rule reports when you may consider replacing: - An `||` operator with `??` - An `||=` operator with `??=` +- Ternary expressions (`?:`) that are equivalent to `||` or `??` with `??` :::caution This rule will not work as expected if [`strictNullChecks`](https://www.typescriptlang.org/tsconfig#strictNullChecks) is not enabled. diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 815415be614d..bb506b38a6ac 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -11,6 +11,7 @@ import { getTypeFlags, isLogicalOrOperator, isNodeEqual, + isNodeOfTypes, isNullLiteral, isPossiblyNullish, isUndefinedIdentifier, @@ -406,14 +407,14 @@ export default createRule({ hasTruthinessCheck = true; if ( - isIdentifierOrMemberExpressionType(node.test.type) && + isIdentifierOrMemberExpression(node.test) && isNodeEqual(node.test, node.consequent) ) { identifierOrMemberExpression = node.test; } else if ( node.test.type === AST_NODE_TYPES.UnaryExpression && node.test.operator === '!' && - isIdentifierOrMemberExpressionType(node.test.argument.type) && + isIdentifierOrMemberExpression(node.test.argument) && isNodeEqual(node.test.argument, node.alternate) ) { identifierOrMemberExpression = node.test.argument; @@ -538,9 +539,7 @@ function isConditionalTest(node: TSESTree.Node): boolean { if ( parent.type === AST_NODE_TYPES.ConditionalExpression && - (parent.consequent === node || - parent.alternate === node || - node.type === AST_NODE_TYPES.UnaryExpression) + (parent.consequent === node || parent.alternate === node) ) { return isConditionalTest(parent); } @@ -588,9 +587,7 @@ function isBooleanConstructorContext( if ( parent.type === AST_NODE_TYPES.ConditionalExpression && - (parent.consequent === node || - parent.alternate === node || - node.type === AST_NODE_TYPES.UnaryExpression) + (parent.consequent === node || parent.alternate === node) ) { return isBooleanConstructorContext(parent, context); }