@@ -10,10 +10,10 @@ import type {
1010} from '@typescript-eslint/utils/ts-eslint' ;
1111
1212import { AST_NODE_TYPES } from '@typescript-eslint/utils' ;
13- import { unionConstituents } from 'ts-api-utils' ;
13+ import { isFalsyType , unionConstituents } from 'ts-api-utils' ;
1414import * as ts from 'typescript' ;
1515
16- import type { ValidOperand } from './gatherLogicalOperands' ;
16+ import type { LastChainOperand , ValidOperand } from './gatherLogicalOperands' ;
1717import type {
1818 PreferOptionalChainMessageIds ,
1919 PreferOptionalChainOptions ,
@@ -31,7 +31,7 @@ import {
3131} from '../../util' ;
3232import { checkNullishAndReport } from './checkNullishAndReport' ;
3333import { compareNodes , NodeComparisonResult } from './compareNodes' ;
34- import { NullishComparisonType } from './gatherLogicalOperands' ;
34+ import { ComparisonType , NullishComparisonType } from './gatherLogicalOperands' ;
3535
3636function includesType (
3737 parserServices : ParserServicesWithTypeInformation ,
@@ -48,6 +48,109 @@ function includesType(
4848 return false ;
4949}
5050
51+ function isAlwaysTruthyOperand (
52+ comparedName : TSESTree . Node ,
53+ nullishComparisonType : NullishComparisonType ,
54+ parserServices : ParserServicesWithTypeInformation ,
55+ ) : boolean {
56+ const ANY_UNKNOWN_FLAGS = ts . TypeFlags . Any | ts . TypeFlags . Unknown ;
57+ const comparedNameType = parserServices . getTypeAtLocation ( comparedName ) ;
58+
59+ if ( isTypeFlagSet ( comparedNameType , ANY_UNKNOWN_FLAGS ) ) {
60+ return false ;
61+ }
62+ switch ( nullishComparisonType ) {
63+ case NullishComparisonType . Boolean :
64+ case NullishComparisonType . NotBoolean : {
65+ const types = unionConstituents ( comparedNameType ) ;
66+ return types . every ( type => ! isFalsyType ( type ) ) ;
67+ }
68+ case NullishComparisonType . NotStrictEqualUndefined :
69+ case NullishComparisonType . NotStrictEqualNull :
70+ case NullishComparisonType . StrictEqualNull :
71+ case NullishComparisonType . StrictEqualUndefined :
72+ return ! isTypeFlagSet (
73+ comparedNameType ,
74+ ts . TypeFlags . Null | ts . TypeFlags . Undefined ,
75+ ) ;
76+ case NullishComparisonType . NotEqualNullOrUndefined :
77+ case NullishComparisonType . EqualNullOrUndefined :
78+ return ! isTypeFlagSet (
79+ comparedNameType ,
80+ ts . TypeFlags . Null | ts . TypeFlags . Undefined ,
81+ ) ;
82+ }
83+ }
84+
85+ function isValidAndLastChainOperand (
86+ ComparisonValueType : TSESTree . Node ,
87+ comparisonType : ComparisonType ,
88+ parserServices : ParserServicesWithTypeInformation ,
89+ ) {
90+ const type = parserServices . getTypeAtLocation ( ComparisonValueType ) ;
91+ const ANY_UNKNOWN_FLAGS = ts . TypeFlags . Any | ts . TypeFlags . Unknown ;
92+
93+ const types = unionConstituents ( type ) ;
94+ switch ( comparisonType ) {
95+ case ComparisonType . Equal : {
96+ const isNullish = types . some ( t =>
97+ isTypeFlagSet (
98+ t ,
99+ ANY_UNKNOWN_FLAGS | ts . TypeFlags . Null | ts . TypeFlags . Undefined ,
100+ ) ,
101+ ) ;
102+ return ! isNullish ;
103+ }
104+ case ComparisonType . StrictEqual : {
105+ const isUndefined = types . some ( t =>
106+ isTypeFlagSet ( t , ANY_UNKNOWN_FLAGS | ts . TypeFlags . Undefined ) ,
107+ ) ;
108+ return ! isUndefined ;
109+ }
110+ case ComparisonType . NotStrictEqual : {
111+ return types . every ( t => isTypeFlagSet ( t , ts . TypeFlags . Undefined ) ) ;
112+ }
113+ case ComparisonType . NotEqual : {
114+ return types . every ( t =>
115+ isTypeFlagSet ( t , ts . TypeFlags . Undefined | ts . TypeFlags . Null ) ,
116+ ) ;
117+ }
118+ }
119+ }
120+ function isValidOrLastChainOperand (
121+ ComparisonValueType : TSESTree . Node ,
122+ comparisonType : ComparisonType ,
123+ parserServices : ParserServicesWithTypeInformation ,
124+ ) {
125+ const type = parserServices . getTypeAtLocation ( ComparisonValueType ) ;
126+ const ANY_UNKNOWN_FLAGS = ts . TypeFlags . Any | ts . TypeFlags . Unknown ;
127+
128+ const types = unionConstituents ( type ) ;
129+ switch ( comparisonType ) {
130+ case ComparisonType . NotEqual : {
131+ const isNullish = types . some ( t =>
132+ isTypeFlagSet (
133+ t ,
134+ ANY_UNKNOWN_FLAGS | ts . TypeFlags . Null | ts . TypeFlags . Undefined ,
135+ ) ,
136+ ) ;
137+ return ! isNullish ;
138+ }
139+ case ComparisonType . NotStrictEqual : {
140+ const isUndefined = types . some ( t =>
141+ isTypeFlagSet ( t , ANY_UNKNOWN_FLAGS | ts . TypeFlags . Undefined ) ,
142+ ) ;
143+ return ! isUndefined ;
144+ }
145+ case ComparisonType . Equal :
146+ return types . every ( t =>
147+ isTypeFlagSet ( t , ts . TypeFlags . Undefined | ts . TypeFlags . Null ) ,
148+ ) ;
149+ case ComparisonType . StrictEqual :
150+ return types . every ( t => isTypeFlagSet ( t , ts . TypeFlags . Undefined ) ) ;
151+ }
152+ }
153+
51154// I hate that these functions are identical aside from the enum values used
52155// I can't think of a good way to reuse the code here in a way that will preserve
53156// the type safety and simplicity.
@@ -65,18 +168,7 @@ const analyzeAndChainOperand: OperandAnalyzer = (
65168 chain ,
66169) => {
67170 switch ( operand . comparisonType ) {
68- case NullishComparisonType . Boolean : {
69- const nextOperand = chain . at ( index + 1 ) ;
70- if (
71- nextOperand ?. comparisonType ===
72- NullishComparisonType . NotStrictEqualNull &&
73- operand . comparedName . type === AST_NODE_TYPES . Identifier
74- ) {
75- return null ;
76- }
77- return [ operand ] ;
78- }
79-
171+ case NullishComparisonType . Boolean :
80172 case NullishComparisonType . NotEqualNullOrUndefined :
81173 return [ operand ] ;
82174
@@ -92,7 +184,8 @@ const analyzeAndChainOperand: OperandAnalyzer = (
92184 return [ operand , nextOperand ] ;
93185 }
94186 if (
95- includesType (
187+ nextOperand &&
188+ ! includesType (
96189 parserServices ,
97190 operand . comparedName ,
98191 ts . TypeFlags . Undefined ,
@@ -101,10 +194,9 @@ const analyzeAndChainOperand: OperandAnalyzer = (
101194 // we know the next operand is not an `undefined` check and that this
102195 // operand includes `undefined` - which means that making this an
103196 // optional chain would change the runtime behavior of the expression
104- return null ;
197+ return [ operand ] ;
105198 }
106-
107- return [ operand ] ;
199+ return null ;
108200 }
109201
110202 case NullishComparisonType . NotStrictEqualUndefined : {
@@ -156,6 +248,7 @@ const analyzeOrChainOperand: OperandAnalyzer = (
156248 ) {
157249 return [ operand , nextOperand ] ;
158250 }
251+
159252 if (
160253 includesType (
161254 parserServices ,
@@ -168,7 +261,6 @@ const analyzeOrChainOperand: OperandAnalyzer = (
168261 // optional chain would change the runtime behavior of the expression
169262 return null ;
170263 }
171-
172264 return [ operand ] ;
173265 }
174266
@@ -207,7 +299,7 @@ const analyzeOrChainOperand: OperandAnalyzer = (
207299 * @returns The range to report.
208300 */
209301function getReportRange (
210- chain : ValidOperand [ ] ,
302+ chain : { node : TSESTree . Expression } [ ] ,
211303 boundary : TSESTree . Range ,
212304 sourceCode : SourceCode ,
213305) : TSESTree . Range {
@@ -247,8 +339,10 @@ function getReportDescriptor(
247339 node : TSESTree . Node ,
248340 operator : '&&' | '||' ,
249341 options : PreferOptionalChainOptions ,
250- chain : ValidOperand [ ] ,
342+ subChain : ValidOperand [ ] ,
343+ lastChain : ( LastChainOperand | ValidOperand ) | undefined ,
251344) : ReportDescriptor < PreferOptionalChainMessageIds > {
345+ const chain = lastChain ? [ ...subChain , lastChain ] : subChain ;
252346 const lastOperand = chain [ chain . length - 1 ] ;
253347
254348 let useSuggestionFixer : boolean ;
@@ -264,6 +358,7 @@ function getReportDescriptor(
264358 // `undefined`, or else we're going to change the final type - which is
265359 // unsafe and might cause downstream type errors.
266360 else if (
361+ lastChain ||
267362 lastOperand . comparisonType === NullishComparisonType . EqualNullOrUndefined ||
268363 lastOperand . comparisonType ===
269364 NullishComparisonType . NotEqualNullOrUndefined ||
@@ -521,10 +616,11 @@ export function analyzeChain(
521616 node : TSESTree . Node ,
522617 operator : TSESTree . LogicalExpression [ 'operator' ] ,
523618 chain : ValidOperand [ ] ,
619+ lastChainOperand ?: LastChainOperand ,
524620) : void {
525621 // need at least 2 operands in a chain for it to be a chain
526622 if (
527- chain . length <= 1 ||
623+ chain . length + ( lastChainOperand ? 1 : 0 ) <= 1 ||
528624 /* istanbul ignore next -- previous checks make this unreachable, but keep it for exhaustiveness check */
529625 operator === '??'
530626 ) {
@@ -544,23 +640,28 @@ export function analyzeChain(
544640 // Things like x !== null && x !== undefined have two nodes, but they are
545641 // one logical unit here, so we'll allow them to be grouped.
546642 let subChain : ( readonly ValidOperand [ ] | ValidOperand ) [ ] = [ ] ;
643+ let lastChain : LastChainOperand | ValidOperand | undefined = undefined ;
547644 const maybeReportThenReset = (
548645 newChainSeed ?: readonly [ ValidOperand , ...ValidOperand [ ] ] ,
549646 ) : void => {
550- if ( subChain . length > 1 ) {
647+ if ( subChain . length + ( lastChain ? 1 : 0 ) > 1 ) {
551648 const subChainFlat = subChain . flat ( ) ;
649+ const maybeNullishNodes = lastChain
650+ ? subChainFlat . map ( ( { node } ) => node )
651+ : subChainFlat . slice ( 0 , - 1 ) . map ( ( { node } ) => node ) ;
552652 checkNullishAndReport (
553653 context ,
554654 parserServices ,
555655 options ,
556- subChainFlat . slice ( 0 , - 1 ) . map ( ( { node } ) => node ) ,
656+ maybeNullishNodes ,
557657 getReportDescriptor (
558658 context . sourceCode ,
559659 parserServices ,
560660 node ,
561661 operator ,
562662 options ,
563663 subChainFlat ,
664+ lastChain ,
564665 ) ,
565666 ) ;
566667 }
@@ -578,6 +679,7 @@ export function analyzeChain(
578679 // ^^^^^^^^^^^ newChainSeed
579680 // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ second chain
580681 subChain = newChainSeed ? [ newChainSeed ] : [ ] ;
682+ lastChain = undefined ;
581683 } ;
582684
583685 for ( let i = 0 ; i < chain . length ; i += 1 ) {
@@ -595,6 +697,35 @@ export function analyzeChain(
595697 // ^^^^^^^ invalid OR chain logical, but still part of
596698 // the chain for combination purposes
597699
700+ if ( lastOperand ) {
701+ const comparisonResult = compareNodes (
702+ lastOperand . comparedName ,
703+ operand . comparedName ,
704+ ) ;
705+ switch ( operand . comparisonType ) {
706+ case NullishComparisonType . StrictEqualUndefined :
707+ case NullishComparisonType . NotStrictEqualUndefined : {
708+ if ( comparisonResult === NodeComparisonResult . Subset ) {
709+ lastChain = operand ;
710+ }
711+ break ;
712+ }
713+ case NullishComparisonType . StrictEqualNull :
714+ case NullishComparisonType . NotStrictEqualNull : {
715+ if (
716+ comparisonResult === NodeComparisonResult . Subset &&
717+ isAlwaysTruthyOperand (
718+ lastOperand . comparedName ,
719+ lastOperand . comparisonType ,
720+ parserServices ,
721+ )
722+ ) {
723+ lastChain = operand ;
724+ }
725+ break ;
726+ }
727+ }
728+ }
598729 maybeReportThenReset ( ) ;
599730 continue ;
600731 }
@@ -624,7 +755,33 @@ export function analyzeChain(
624755 subChain . push ( currentOperand ) ;
625756 }
626757 }
758+ const lastOperand = subChain . flat ( ) . at ( - 1 ) ;
627759
760+ if ( lastOperand && lastChainOperand ) {
761+ const comparisonResult = compareNodes (
762+ lastOperand . comparedName ,
763+ lastChainOperand . comparedName ,
764+ ) ;
765+ const isValidLastChainOperand =
766+ operator === '&&'
767+ ? isValidAndLastChainOperand
768+ : isValidOrLastChainOperand ;
769+ if (
770+ comparisonResult === NodeComparisonResult . Subset &&
771+ ( isAlwaysTruthyOperand (
772+ lastOperand . comparedName ,
773+ lastOperand . comparisonType ,
774+ parserServices ,
775+ ) ||
776+ isValidLastChainOperand (
777+ lastChainOperand . comparisonValue ,
778+ lastChainOperand . comparisonType ,
779+ parserServices ,
780+ ) )
781+ ) {
782+ lastChain = lastChainOperand ;
783+ }
784+ }
628785 // check the leftovers
629786 maybeReportThenReset ( ) ;
630787}
0 commit comments