From 5c5a4c2e235f67c488ad07ef98b057b4b98ef3f8 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Mon, 26 Feb 2024 22:02:00 -0600 Subject: [PATCH 01/17] ignore right --- .../gatherLogicalOperands.ts | 9 +++++---- .../prefer-optional-chain.test.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts index 986b2e5f8575..92fdffb62fb5 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts @@ -61,7 +61,7 @@ type Operand = ValidOperand | InvalidOperand; const NULLISH_FLAGS = ts.TypeFlags.Null | ts.TypeFlags.Undefined; function isValidFalseBooleanCheckType( node: TSESTree.Node, - operator: TSESTree.LogicalExpression['operator'], + operatorNode: TSESTree.LogicalExpression, checkType: 'true' | 'false', parserServices: ParserServicesWithTypeInformation, options: PreferOptionalChainOptions, @@ -69,6 +69,7 @@ function isValidFalseBooleanCheckType( const type = parserServices.getTypeAtLocation(node); const types = unionTypeParts(type); + const { operator } = operatorNode; const disallowFalseyLiteral = (operator === '||' && checkType === 'false') || (operator === '&&' && checkType === 'true'); @@ -94,7 +95,7 @@ function isValidFalseBooleanCheckType( } } - if (options.requireNullish === true) { + if (options.requireNullish === true && operatorNode.right !== node) { return types.some(t => isTypeFlagSet(t, NULLISH_FLAGS)); } @@ -258,7 +259,7 @@ export function gatherLogicalOperands( operand.operator === '!' && isValidFalseBooleanCheckType( operand.argument, - node.operator, + node, 'false', parserServices, options, @@ -285,7 +286,7 @@ export function gatherLogicalOperands( if ( isValidFalseBooleanCheckType( operand, - node.operator, + node, 'true', parserServices, options, diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index cee379d45ac2..80cd6d7eee87 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -1866,6 +1866,16 @@ describe('hand-crafted cases', () => { ], }, + // requireNullish + { + code: ` + declare const thing1: string | null; + thing1 && thing1.toString(); + `, + options: [{ requireNullish: false }], + errors: [{ messageId: `preferOptionalChain` }], + }, + // allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing { code: ` From d74cc6c4638cd6e8f261c737f2e36cb8bd7e8431 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Mon, 26 Feb 2024 22:11:19 -0600 Subject: [PATCH 02/17] invert check --- .../rules/prefer-optional-chain-utils/gatherLogicalOperands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts index 92fdffb62fb5..81008ebe81a3 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts @@ -95,7 +95,7 @@ function isValidFalseBooleanCheckType( } } - if (options.requireNullish === true && operatorNode.right !== node) { + if (options.requireNullish === true && operatorNode.left === node) { return types.some(t => isTypeFlagSet(t, NULLISH_FLAGS)); } From 2e3eec3d1c3c88575b86f0282ddca064cb5b0b62 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Tue, 27 Feb 2024 07:12:12 -0600 Subject: [PATCH 03/17] invert --- .../rules/prefer-optional-chain/prefer-optional-chain.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index 80cd6d7eee87..a1b6cebcdff7 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -1872,7 +1872,7 @@ describe('hand-crafted cases', () => { declare const thing1: string | null; thing1 && thing1.toString(); `, - options: [{ requireNullish: false }], + options: [{ requireNullish: true }], errors: [{ messageId: `preferOptionalChain` }], }, From 3e22435652ac94644b448469322ce663de3aee4b Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Tue, 27 Feb 2024 07:48:48 -0600 Subject: [PATCH 04/17] add failing case --- .../prefer-optional-chain.test.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index a1b6cebcdff7..4e7bc8b6540a 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -786,22 +786,22 @@ describe('hand-crafted cases', () => { declare const x: string; x && x.length; `, - options: [ - { - requireNullish: true, - }, - ], + options: [{ requireNullish: true }], }, { code: ` declare const x: string | number | boolean | object; x && x.toString(); `, - options: [ - { - requireNullish: true, - }, - ], + options: [{ requireNullish: true }], + }, + { + code: ` + declare const foo: { bar: string }; + foo && foo.bar && foo.bar.toString(); + `, + options: [{ requireNullish: true }], + only: true, }, { code: ` From 14d3aebf95f0b81524bc02b94f446925c6d5a2f5 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Tue, 27 Feb 2024 17:04:22 -0600 Subject: [PATCH 05/17] move operator check --- .../prefer-optional-chain-utils/gatherLogicalOperands.ts | 7 +++++-- .../prefer-optional-chain/prefer-optional-chain.test.ts | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts index 81008ebe81a3..4c360fbd3261 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts @@ -95,8 +95,11 @@ function isValidFalseBooleanCheckType( } } - if (options.requireNullish === true && operatorNode.left === node) { - return types.some(t => isTypeFlagSet(t, NULLISH_FLAGS)); + if (options.requireNullish === true) { + return ( + operatorNode.right === node || + types.some(t => isTypeFlagSet(t, NULLISH_FLAGS)) + ); } let allowedFlags = NULLISH_FLAGS | ts.TypeFlags.Object; diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index 4e7bc8b6540a..3019216ef155 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -801,7 +801,6 @@ describe('hand-crafted cases', () => { foo && foo.bar && foo.bar.toString(); `, options: [{ requireNullish: true }], - only: true, }, { code: ` From 0e4fcf4be53fc7777677cc1e7dc84d6b914ab8ab Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Mon, 4 Mar 2024 06:35:52 -0600 Subject: [PATCH 06/17] WIP --- .../prefer-optional-chain/prefer-optional-chain.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index 3019216ef155..430cd5d8eeaa 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -1874,6 +1874,15 @@ describe('hand-crafted cases', () => { options: [{ requireNullish: true }], errors: [{ messageId: `preferOptionalChain` }], }, + { + code: ` + declare const thing1: string | null; + thing1 && thing1.toString() && true; + `, + options: [{ requireNullish: true }], + errors: [{ messageId: `preferOptionalChain` }], + only: true, + }, // allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing { From 3969122e3d713f5eddd03cbbacb3bcab3d8cb458 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Thu, 7 Mar 2024 06:56:22 -0600 Subject: [PATCH 07/17] WIP --- .../gatherLogicalOperands.ts | 50 ++++++++++--------- .../prefer-optional-chain.test.ts | 2 +- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts index 4c360fbd3261..7046d31edde5 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts @@ -95,31 +95,35 @@ function isValidFalseBooleanCheckType( } } - if (options.requireNullish === true) { - return ( - operatorNode.right === node || - types.some(t => isTypeFlagSet(t, NULLISH_FLAGS)) - ); - } + /* if ( + options.requireNullish === true && + !types.some(t => isTypeFlagSet(t, NULLISH_FLAGS)) + ) { + return; + }*/ let allowedFlags = NULLISH_FLAGS | ts.TypeFlags.Object; - if (options.checkAny === true) { - allowedFlags |= ts.TypeFlags.Any; - } - if (options.checkUnknown === true) { - allowedFlags |= ts.TypeFlags.Unknown; - } - if (options.checkString === true) { - allowedFlags |= ts.TypeFlags.StringLike; - } - if (options.checkNumber === true) { - allowedFlags |= ts.TypeFlags.NumberLike; - } - if (options.checkBoolean === true) { - allowedFlags |= ts.TypeFlags.BooleanLike; - } - if (options.checkBigInt === true) { - allowedFlags |= ts.TypeFlags.BigIntLike; + if (options.requireNullish === true) { + allowedFlags = NULLISH_FLAGS; + } else { + if (options.checkAny === true) { + allowedFlags |= ts.TypeFlags.Any; + } + if (options.checkUnknown === true) { + allowedFlags |= ts.TypeFlags.Unknown; + } + if (options.checkString === true) { + allowedFlags |= ts.TypeFlags.StringLike; + } + if (options.checkNumber === true) { + allowedFlags |= ts.TypeFlags.NumberLike; + } + if (options.checkBoolean === true) { + allowedFlags |= ts.TypeFlags.BooleanLike; + } + if (options.checkBigInt === true) { + allowedFlags |= ts.TypeFlags.BigIntLike; + } } return types.every(t => isTypeFlagSet(t, allowedFlags)); } diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index 430cd5d8eeaa..f92c979ef24f 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -1873,6 +1873,7 @@ describe('hand-crafted cases', () => { `, options: [{ requireNullish: true }], errors: [{ messageId: `preferOptionalChain` }], + only: true, }, { code: ` @@ -1881,7 +1882,6 @@ describe('hand-crafted cases', () => { `, options: [{ requireNullish: true }], errors: [{ messageId: `preferOptionalChain` }], - only: true, }, // allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing From d6ff24c91282af8c9abd1e19a86019e6fddea7fa Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Mon, 11 Mar 2024 07:39:31 -0500 Subject: [PATCH 08/17] pull check out --- .../analyzeChain.ts | 33 +++++++----- .../checkNullishAndReport.ts | 38 ++++++++++++++ .../gatherLogicalOperands.ts | 52 +++++++------------ .../src/rules/prefer-optional-chain.ts | 5 +- .../prefer-optional-chain.test.ts | 1 - 5 files changed, 81 insertions(+), 48 deletions(-) create mode 100644 packages/eslint-plugin/src/rules/prefer-optional-chain-utils/checkNullishAndReport.ts diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index ec1a811ac3c1..d11d3a5510db 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -23,6 +23,7 @@ import { import { compareNodes, NodeComparisonResult } from './compareNodes'; import type { ValidOperand } from './gatherLogicalOperands'; import { NullishComparisonType } from './gatherLogicalOperands'; +import { checkNullishAndReport } from './checkNullishAndReport'; import type { PreferOptionalChainMessageIds, PreferOptionalChainOptions, @@ -490,20 +491,26 @@ export function analyzeChain( newChainSeed?: readonly ValidOperand[], ): void => { if (subChain.length > 1) { - context.report({ - messageId: 'preferOptionalChain', - loc: { - start: subChain[0].node.loc.start, - end: subChain[subChain.length - 1].node.loc.end, + checkNullishAndReport( + context, + parserServices, + options, + subChain.map(({ node }) => node), + { + messageId: 'preferOptionalChain', + loc: { + start: subChain[0].node.loc.start, + end: subChain[subChain.length - 1].node.loc.end, + }, + ...getFixer( + context.sourceCode, + parserServices, + operator, + options, + subChain, + ), }, - ...getFixer( - context.sourceCode, - parserServices, - operator, - options, - subChain, - ), - }); + ); } // we've reached the end of a chain of logical expressions diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/checkNullishAndReport.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/checkNullishAndReport.ts new file mode 100644 index 000000000000..2df8e4f5d6af --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/checkNullishAndReport.ts @@ -0,0 +1,38 @@ +import { isTypeFlagSet } from '@typescript-eslint/type-utils'; +import type { + ReportDescriptor, + RuleContext, +} from '@typescript-eslint/utils/ts-eslint'; +import { unionTypeParts } from 'ts-api-utils'; +import * as ts from 'typescript'; + +import type { + PreferOptionalChainMessageIds, + PreferOptionalChainOptions, +} from './PreferOptionalChainOptions'; +import { + ParserServicesWithTypeInformation, + TSESTree, +} from '@typescript-eslint/typescript-estree'; + +export function checkNullishAndReport( + context: RuleContext< + PreferOptionalChainMessageIds, + [PreferOptionalChainOptions] + >, + parserServices: ParserServicesWithTypeInformation, + { requireNullish }: PreferOptionalChainOptions, + maybeNullishNodes: TSESTree.Expression[], + descriptor: ReportDescriptor, +): void { + if ( + !requireNullish || + maybeNullishNodes.some(node => + unionTypeParts(parserServices.getTypeAtLocation(node)).some(t => + isTypeFlagSet(t, ts.TypeFlags.Null | ts.TypeFlags.Undefined), + ), + ) + ) { + context.report(descriptor); + } +} diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts index 7046d31edde5..8a4bd7097c30 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts @@ -61,7 +61,7 @@ type Operand = ValidOperand | InvalidOperand; const NULLISH_FLAGS = ts.TypeFlags.Null | ts.TypeFlags.Undefined; function isValidFalseBooleanCheckType( node: TSESTree.Node, - operatorNode: TSESTree.LogicalExpression, + operator: TSESTree.LogicalExpression['operator'], checkType: 'true' | 'false', parserServices: ParserServicesWithTypeInformation, options: PreferOptionalChainOptions, @@ -69,7 +69,6 @@ function isValidFalseBooleanCheckType( const type = parserServices.getTypeAtLocation(node); const types = unionTypeParts(type); - const { operator } = operatorNode; const disallowFalseyLiteral = (operator === '||' && checkType === 'false') || (operator === '&&' && checkType === 'true'); @@ -95,35 +94,24 @@ function isValidFalseBooleanCheckType( } } - /* if ( - options.requireNullish === true && - !types.some(t => isTypeFlagSet(t, NULLISH_FLAGS)) - ) { - return; - }*/ - let allowedFlags = NULLISH_FLAGS | ts.TypeFlags.Object; - if (options.requireNullish === true) { - allowedFlags = NULLISH_FLAGS; - } else { - if (options.checkAny === true) { - allowedFlags |= ts.TypeFlags.Any; - } - if (options.checkUnknown === true) { - allowedFlags |= ts.TypeFlags.Unknown; - } - if (options.checkString === true) { - allowedFlags |= ts.TypeFlags.StringLike; - } - if (options.checkNumber === true) { - allowedFlags |= ts.TypeFlags.NumberLike; - } - if (options.checkBoolean === true) { - allowedFlags |= ts.TypeFlags.BooleanLike; - } - if (options.checkBigInt === true) { - allowedFlags |= ts.TypeFlags.BigIntLike; - } + if (options.checkAny === true) { + allowedFlags |= ts.TypeFlags.Any; + } + if (options.checkUnknown === true) { + allowedFlags |= ts.TypeFlags.Unknown; + } + if (options.checkString === true) { + allowedFlags |= ts.TypeFlags.StringLike; + } + if (options.checkNumber === true) { + allowedFlags |= ts.TypeFlags.NumberLike; + } + if (options.checkBoolean === true) { + allowedFlags |= ts.TypeFlags.BooleanLike; + } + if (options.checkBigInt === true) { + allowedFlags |= ts.TypeFlags.BigIntLike; } return types.every(t => isTypeFlagSet(t, allowedFlags)); } @@ -266,7 +254,7 @@ export function gatherLogicalOperands( operand.operator === '!' && isValidFalseBooleanCheckType( operand.argument, - node, + node.operator, 'false', parserServices, options, @@ -293,7 +281,7 @@ export function gatherLogicalOperands( if ( isValidFalseBooleanCheckType( operand, - node, + node.operator, 'true', parserServices, options, diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 605045b99fd2..c571df3f0c2f 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -19,6 +19,7 @@ import type { PreferOptionalChainMessageIds, PreferOptionalChainOptions, } from './prefer-optional-chain-utils/PreferOptionalChainOptions'; +import { checkNullishAndReport } from './prefer-optional-chain-utils/checkNullishAndReport'; export default createRule< [PreferOptionalChainOptions], @@ -141,9 +142,9 @@ export default createRule< return leftPrecedence < OperatorPrecedence.LeftHandSide; } - context.report({ - node: parentNode, + checkNullishAndReport(context, parserServices, options, [leftNode], { messageId: 'preferOptionalChain', + node: parentNode, suggest: [ { messageId: 'optionalChainSuggest', diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index f92c979ef24f..a75146b717a3 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -1873,7 +1873,6 @@ describe('hand-crafted cases', () => { `, options: [{ requireNullish: true }], errors: [{ messageId: `preferOptionalChain` }], - only: true, }, { code: ` From 2833796787660f6008ec51eba19285943eb712aa Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Mon, 11 Mar 2024 07:41:19 -0500 Subject: [PATCH 09/17] lint --- .../src/rules/prefer-optional-chain-utils/analyzeChain.ts | 2 +- .../prefer-optional-chain-utils/checkNullishAndReport.ts | 8 ++++---- packages/eslint-plugin/src/rules/prefer-optional-chain.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index d11d3a5510db..445f74bfc6c3 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -20,10 +20,10 @@ import { NullThrowsReasons, OperatorPrecedence, } from '../../util'; +import { checkNullishAndReport } from './checkNullishAndReport'; import { compareNodes, NodeComparisonResult } from './compareNodes'; import type { ValidOperand } from './gatherLogicalOperands'; import { NullishComparisonType } from './gatherLogicalOperands'; -import { checkNullishAndReport } from './checkNullishAndReport'; import type { PreferOptionalChainMessageIds, PreferOptionalChainOptions, diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/checkNullishAndReport.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/checkNullishAndReport.ts index 2df8e4f5d6af..404b3736040a 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/checkNullishAndReport.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/checkNullishAndReport.ts @@ -1,4 +1,8 @@ import { isTypeFlagSet } from '@typescript-eslint/type-utils'; +import type { + ParserServicesWithTypeInformation, + TSESTree, +} from '@typescript-eslint/utils'; import type { ReportDescriptor, RuleContext, @@ -10,10 +14,6 @@ import type { PreferOptionalChainMessageIds, PreferOptionalChainOptions, } from './PreferOptionalChainOptions'; -import { - ParserServicesWithTypeInformation, - TSESTree, -} from '@typescript-eslint/typescript-estree'; export function checkNullishAndReport( context: RuleContext< diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index c571df3f0c2f..18acdd72caf2 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -10,6 +10,7 @@ import { OperatorPrecedence, } from '../util'; import { analyzeChain } from './prefer-optional-chain-utils/analyzeChain'; +import { checkNullishAndReport } from './prefer-optional-chain-utils/checkNullishAndReport'; import type { ValidOperand } from './prefer-optional-chain-utils/gatherLogicalOperands'; import { gatherLogicalOperands, @@ -19,7 +20,6 @@ import type { PreferOptionalChainMessageIds, PreferOptionalChainOptions, } from './prefer-optional-chain-utils/PreferOptionalChainOptions'; -import { checkNullishAndReport } from './prefer-optional-chain-utils/checkNullishAndReport'; export default createRule< [PreferOptionalChainOptions], From 386d63c983af58ebff80ab4f501b77c1eb2906b1 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Tue, 12 Mar 2024 07:01:57 -0500 Subject: [PATCH 10/17] add tests --- .../prefer-optional-chain.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index a75146b717a3..3bdea71e9c8b 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -788,6 +788,13 @@ describe('hand-crafted cases', () => { `, options: [{ requireNullish: true }], }, + { + code: ` + declare const foo: string; + foo && foo.toString(); + `, + options: [{ requireNullish: true }], + }, { code: ` declare const x: string | number | boolean | object; @@ -802,6 +809,34 @@ describe('hand-crafted cases', () => { `, options: [{ requireNullish: true }], }, + { + code: ` + declare const foo: string; + foo && foo.toString() && foo.toString(); + `, + options: [{ requireNullish: true }], + }, + { + code: ` + declare const foo: { bar: string | null | undefined } | null | undefined; + foo && foo.bar && foo.bar.toString(); + `, + options: [{ requireNullish: true }], + }, + { + code: ` + declare const foo: { bar: string }; + foo && foo.bar && foo.bar.toString(); + `, + options: [{ requireNullish: true }], + }, + { + code: ` + declare const foo: { bar: string }; + foo && foo.bar && foo.bar.toString() && foo.bar.toString(); + `, + options: [{ requireNullish: true }], + }, { code: ` declare const x: any; @@ -1882,6 +1917,22 @@ describe('hand-crafted cases', () => { options: [{ requireNullish: true }], errors: [{ messageId: `preferOptionalChain` }], }, + { + code: ` + declare const foo: string | null; + foo && foo.toString() && foo.toString(); + `, + options: [{ requireNullish: true }], + errors: [{ messageId: `preferOptionalChain` }], + }, + { + code: ` + declare const foo: { bar: string | null | undefined } | null | undefined; + foo && foo.bar && foo.bar.toString() && foo.bar.toString(); + `, + options: [{ requireNullish: true }], + errors: [{ messageId: 'preferOptionalChain' }], + }, // allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing { From 27384051d6f2e4bd7443ded2f76d096499b8f8a4 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Tue, 12 Mar 2024 07:03:33 -0500 Subject: [PATCH 11/17] add output --- .../rules/prefer-optional-chain/prefer-optional-chain.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index 3bdea71e9c8b..f5ab95fc747e 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -1930,6 +1930,10 @@ describe('hand-crafted cases', () => { declare const foo: { bar: string | null | undefined } | null | undefined; foo && foo.bar && foo.bar.toString() && foo.bar.toString(); `, + output: ` + declare const foo: { bar: string | null | undefined } | null | undefined; + foo?.bar?.toString() && foo.bar.toString(); + `, options: [{ requireNullish: true }], errors: [{ messageId: 'preferOptionalChain' }], }, From 5e04ce9d38e8eb41daf5d70d559af057356f9c9f Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Tue, 12 Mar 2024 07:03:53 -0500 Subject: [PATCH 12/17] quotes --- .../prefer-optional-chain/prefer-optional-chain.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index f5ab95fc747e..478d5d3eb376 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -1907,7 +1907,7 @@ describe('hand-crafted cases', () => { thing1 && thing1.toString(); `, options: [{ requireNullish: true }], - errors: [{ messageId: `preferOptionalChain` }], + errors: [{ messageId: 'preferOptionalChain' }], }, { code: ` @@ -1915,7 +1915,7 @@ describe('hand-crafted cases', () => { thing1 && thing1.toString() && true; `, options: [{ requireNullish: true }], - errors: [{ messageId: `preferOptionalChain` }], + errors: [{ messageId: 'preferOptionalChain' }], }, { code: ` @@ -1923,7 +1923,7 @@ describe('hand-crafted cases', () => { foo && foo.toString() && foo.toString(); `, options: [{ requireNullish: true }], - errors: [{ messageId: `preferOptionalChain` }], + errors: [{ messageId: 'preferOptionalChain' }], }, { code: ` From c9f0c62ce438d6e8daa8f5a7342148dc5d7451dd Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Tue, 12 Mar 2024 07:06:27 -0500 Subject: [PATCH 13/17] finish tests --- .../prefer-optional-chain.test.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index 478d5d3eb376..b1c222e63df8 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -816,20 +816,6 @@ describe('hand-crafted cases', () => { `, options: [{ requireNullish: true }], }, - { - code: ` - declare const foo: { bar: string | null | undefined } | null | undefined; - foo && foo.bar && foo.bar.toString(); - `, - options: [{ requireNullish: true }], - }, - { - code: ` - declare const foo: { bar: string }; - foo && foo.bar && foo.bar.toString(); - `, - options: [{ requireNullish: true }], - }, { code: ` declare const foo: { bar: string }; @@ -1925,6 +1911,18 @@ describe('hand-crafted cases', () => { options: [{ requireNullish: true }], errors: [{ messageId: 'preferOptionalChain' }], }, + { + code: ` + declare const foo: { bar: string | null | undefined } | null | undefined; + foo && foo.bar && foo.bar.toString(); + `, + output: ` + declare const foo: { bar: string | null | undefined } | null | undefined; + foo?.bar?.toString(); + `, + options: [{ requireNullish: true }], + errors: [{ messageId: 'preferOptionalChain' }], + }, { code: ` declare const foo: { bar: string | null | undefined } | null | undefined; From ed4aaa7c9aec48cd0e8083a27df6c03003643ab3 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Tue, 12 Mar 2024 23:14:54 -0500 Subject: [PATCH 14/17] remove last child --- .../src/rules/prefer-optional-chain-utils/analyzeChain.ts | 2 +- .../prefer-optional-chain/prefer-optional-chain.test.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index 445f74bfc6c3..99a58b68bb8c 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -495,7 +495,7 @@ export function analyzeChain( context, parserServices, options, - subChain.map(({ node }) => node), + subChain.slice(0, -1).map(({ node }) => node), { messageId: 'preferOptionalChain', loc: { diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index b1c222e63df8..bdc2dd9a05e0 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -823,6 +823,13 @@ describe('hand-crafted cases', () => { `, options: [{ requireNullish: true }], }, + { + code: ` + declare const foo1: { bar: string | null }; + foo1 && foo1.bar; + `, + options: [{ requireNullish: true }], + }, { code: ` declare const x: any; From fd2b38f4911ec7eac4abc64c98b2ad947fb658be Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Sat, 16 Mar 2024 08:41:07 -0500 Subject: [PATCH 15/17] add a few more tests --- .../prefer-optional-chain.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index bdc2dd9a05e0..f4455779869c 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -830,6 +830,21 @@ describe('hand-crafted cases', () => { `, options: [{ requireNullish: true }], }, + { + code: ` + declare const foo: string; + (foo || {}).toString(); + `, + options: [{ requireNullish: true }], + }, + + { + code: ` + declare const foo: string | null; + (foo || 'a' || {}).toString(); + `, + options: [{ requireNullish: true }], + }, { code: ` declare const x: any; @@ -1942,6 +1957,30 @@ describe('hand-crafted cases', () => { options: [{ requireNullish: true }], errors: [{ messageId: 'preferOptionalChain' }], }, + { + code: ` + declare const foo: string | null; + (foo || {}).toString(); + `, + options: [{ requireNullish: true }], + errors: [{ messageId: 'preferOptionalChain' }], + }, + { + code: ` + declare const foo: string; + (foo || undefined || {}).toString(); + `, + options: [{ requireNullish: true }], + errors: [{ messageId: 'preferOptionalChain' }], + }, + { + code: ` + declare const foo: string | null; + (foo || undefined || {}).toString(); + `, + options: [{ requireNullish: true }], + errors: [{ messageId: 'preferOptionalChain' }], + }, // allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing { From 2bba0ac01b4bc8e5b5a1a722a0c07a5c8b588277 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Sun, 7 Apr 2024 12:24:53 -0500 Subject: [PATCH 16/17] remove skipvalidation --- packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx b/packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx index 47fcec6d598f..ee7aa6abf42f 100644 --- a/packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx +++ b/packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx @@ -265,7 +265,7 @@ When this option is `true` the rule will skip operands that are not typed with ` -```ts option='{ "requireNullish": true }' skipValidation +```ts option='{ "requireNullish": true }' declare const thing1: string | null; thing1 && thing1.toString(); ``` From 15d7dbc067acd6733e62998f7f75365cf019b2d0 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Sun, 7 Apr 2024 16:39:07 -0500 Subject: [PATCH 17/17] update snapshots --- .../docs-eslint-output-snapshots/prefer-optional-chain.shot | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-optional-chain.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-optional-chain.shot index 36130171307f..980b3f8058df 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-optional-chain.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-optional-chain.shot @@ -202,6 +202,7 @@ Options: { "requireNullish": true } declare const thing1: string | null; thing1 && thing1.toString(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~ Prefer using an optional chain expression instead, as it's more concise and easier to read. " `;