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); } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy