Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
fix(eslint-plugin): [prefer-optional-chain] include mixed "nullish comparison style" chains in checks#11533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
cfdd53633e6c716bf2f643aa3b0451d956f8c3de3a5987dfcbd5d28915b7fc87ffa803df6809a165e914b7d2e022adc3da7fe1f3390c13fa609d1aa9aa435abefd7a0c59f77faa793f22a5798bf71611ff8e07cfFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -317,6 +317,7 @@ export default createRule<Options, MessageIds>({ | ||
| const declarations = toString.getDeclarations(); | ||
| // eslint-disable-next-line @typescript-eslint/prefer-optional-chain | ||
| if (declarations == null || declarations.length !== 1) { | ||
Comment on lines +320 to 321 ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more.
When the new logic was applied, this code But I think the original code is more readable and intuitive. Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. If the choices are these:
Personal anecodtal thoughts: I find the updated form more readable. I think when | ||
| // If there are multiple declarations, at least one of them must not be | ||
| // the default object toString. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -10,10 +10,10 @@ import type { | ||
| } from '@typescript-eslint/utils/ts-eslint'; | ||
| import { AST_NODE_TYPES } from '@typescript-eslint/utils'; | ||
| import {isFalsyType,unionConstituents } from 'ts-api-utils'; | ||
| import * as ts from 'typescript'; | ||
| import type {LastChainOperand,ValidOperand } from './gatherLogicalOperands'; | ||
| import type { | ||
| PreferOptionalChainMessageIds, | ||
| PreferOptionalChainOptions, | ||
| @@ -31,7 +31,7 @@ import { | ||
| } from '../../util'; | ||
| import { checkNullishAndReport } from './checkNullishAndReport'; | ||
| import { compareNodes, NodeComparisonResult } from './compareNodes'; | ||
| import {ComparisonType,NullishComparisonType } from './gatherLogicalOperands'; | ||
| function includesType( | ||
| parserServices: ParserServicesWithTypeInformation, | ||
| @@ -48,6 +48,109 @@ function includesType( | ||
| return false; | ||
| } | ||
| function isAlwaysTruthyOperand( | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I'm not convinced this function is necessary.
But if we know Member
| ||
| comparedName: TSESTree.Node, | ||
| nullishComparisonType: NullishComparisonType, | ||
| parserServices: ParserServicesWithTypeInformation, | ||
| ): boolean { | ||
| const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; | ||
| const comparedNameType = parserServices.getTypeAtLocation(comparedName); | ||
| if (isTypeFlagSet(comparedNameType, ANY_UNKNOWN_FLAGS)) { | ||
| return false; | ||
| } | ||
| switch (nullishComparisonType) { | ||
| case NullishComparisonType.Boolean: | ||
| case NullishComparisonType.NotBoolean: { | ||
| const types = unionConstituents(comparedNameType); | ||
| return types.every(type => !isFalsyType(type)); | ||
| } | ||
| case NullishComparisonType.NotStrictEqualUndefined: | ||
| case NullishComparisonType.NotStrictEqualNull: | ||
| case NullishComparisonType.StrictEqualNull: | ||
| case NullishComparisonType.StrictEqualUndefined: | ||
| return !isTypeFlagSet( | ||
| comparedNameType, | ||
| ts.TypeFlags.Null | ts.TypeFlags.Undefined, | ||
| ); | ||
| case NullishComparisonType.NotEqualNullOrUndefined: | ||
| case NullishComparisonType.EqualNullOrUndefined: | ||
| return !isTypeFlagSet( | ||
| comparedNameType, | ||
| ts.TypeFlags.Null | ts.TypeFlags.Undefined, | ||
| ); | ||
| } | ||
| } | ||
| function isValidAndLastChainOperand( | ||
| ComparisonValueType: TSESTree.Node, | ||
| comparisonType: ComparisonType, | ||
| parserServices: ParserServicesWithTypeInformation, | ||
| ) { | ||
| const type = parserServices.getTypeAtLocation(ComparisonValueType); | ||
| const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; | ||
| const types = unionConstituents(type); | ||
| switch (comparisonType) { | ||
| case ComparisonType.Equal: { | ||
| const isNullish = types.some(t => | ||
| isTypeFlagSet( | ||
| t, | ||
| ANY_UNKNOWN_FLAGS | ts.TypeFlags.Null | ts.TypeFlags.Undefined, | ||
| ), | ||
| ); | ||
| return !isNullish; | ||
| } | ||
| case ComparisonType.StrictEqual: { | ||
| const isUndefined = types.some(t => | ||
| isTypeFlagSet(t, ANY_UNKNOWN_FLAGS | ts.TypeFlags.Undefined), | ||
| ); | ||
| return !isUndefined; | ||
| } | ||
| case ComparisonType.NotStrictEqual: { | ||
| return types.every(t => isTypeFlagSet(t, ts.TypeFlags.Undefined)); | ||
| } | ||
| case ComparisonType.NotEqual: { | ||
| return types.every(t => | ||
| isTypeFlagSet(t, ts.TypeFlags.Undefined | ts.TypeFlags.Null), | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| function isValidOrLastChainOperand( | ||
| ComparisonValueType: TSESTree.Node, | ||
| comparisonType: ComparisonType, | ||
| parserServices: ParserServicesWithTypeInformation, | ||
| ) { | ||
| const type = parserServices.getTypeAtLocation(ComparisonValueType); | ||
| const ANY_UNKNOWN_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown; | ||
| const types = unionConstituents(type); | ||
| switch (comparisonType) { | ||
| case ComparisonType.NotEqual: { | ||
| const isNullish = types.some(t => | ||
| isTypeFlagSet( | ||
| t, | ||
| ANY_UNKNOWN_FLAGS | ts.TypeFlags.Null | ts.TypeFlags.Undefined, | ||
| ), | ||
| ); | ||
| return !isNullish; | ||
| } | ||
| case ComparisonType.NotStrictEqual: { | ||
| const isUndefined = types.some(t => | ||
| isTypeFlagSet(t, ANY_UNKNOWN_FLAGS | ts.TypeFlags.Undefined), | ||
| ); | ||
| return !isUndefined; | ||
| } | ||
| case ComparisonType.Equal: | ||
| return types.every(t => | ||
| isTypeFlagSet(t, ts.TypeFlags.Undefined | ts.TypeFlags.Null), | ||
| ); | ||
| case ComparisonType.StrictEqual: | ||
| return types.every(t => isTypeFlagSet(t, ts.TypeFlags.Undefined)); | ||
| } | ||
| } | ||
| // I hate that these functions are identical aside from the enum values used | ||
| // I can't think of a good way to reuse the code here in a way that will preserve | ||
| // the type safety and simplicity. | ||
| @@ -65,18 +168,7 @@ const analyzeAndChainOperand: OperandAnalyzer = ( | ||
| chain, | ||
| ) => { | ||
| switch (operand.comparisonType) { | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. This logic moved to line 700. Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Thanks for these tips, they made reviewing this (already, by necessity, complex) logic easier. 🙂 | ||
| case NullishComparisonType.Boolean: | ||
| case NullishComparisonType.NotEqualNullOrUndefined: | ||
| return [operand]; | ||
| @@ -92,7 +184,8 @@ const analyzeAndChainOperand: OperandAnalyzer = ( | ||
| return [operand, nextOperand]; | ||
| } | ||
| if ( | ||
| nextOperand && | ||
| !includesType( | ||
| parserServices, | ||
| operand.comparedName, | ||
| ts.TypeFlags.Undefined, | ||
| @@ -101,10 +194,9 @@ const analyzeAndChainOperand: OperandAnalyzer = ( | ||
| // we know the next operand is not an `undefined` check and that this | ||
| // operand includes `undefined` - which means that making this an | ||
| // optional chain would change the runtime behavior of the expression | ||
| return[operand]; | ||
| } | ||
| return null; | ||
| } | ||
| case NullishComparisonType.NotStrictEqualUndefined: { | ||
| @@ -156,6 +248,7 @@ const analyzeOrChainOperand: OperandAnalyzer = ( | ||
| ) { | ||
| return [operand, nextOperand]; | ||
| } | ||
| if ( | ||
| includesType( | ||
| parserServices, | ||
| @@ -168,7 +261,6 @@ const analyzeOrChainOperand: OperandAnalyzer = ( | ||
| // optional chain would change the runtime behavior of the expression | ||
| return null; | ||
| } | ||
| return [operand]; | ||
| } | ||
| @@ -207,7 +299,7 @@ const analyzeOrChainOperand: OperandAnalyzer = ( | ||
| * @returns The range to report. | ||
| */ | ||
| function getReportRange( | ||
| chain:{ node: TSESTree.Expression }[], | ||
| boundary: TSESTree.Range, | ||
| sourceCode: SourceCode, | ||
| ): TSESTree.Range { | ||
| @@ -247,8 +339,10 @@ function getReportDescriptor( | ||
| node: TSESTree.Node, | ||
| operator: '&&' | '||', | ||
| options: PreferOptionalChainOptions, | ||
| subChain: ValidOperand[], | ||
| lastChain: (LastChainOperand | ValidOperand) | undefined, | ||
| ): ReportDescriptor<PreferOptionalChainMessageIds> { | ||
| const chain = lastChain ? [...subChain, lastChain] : subChain; | ||
| const lastOperand = chain[chain.length - 1]; | ||
| let useSuggestionFixer: boolean; | ||
| @@ -264,6 +358,7 @@ function getReportDescriptor( | ||
| // `undefined`, or else we're going to change the final type - which is | ||
| // unsafe and might cause downstream type errors. | ||
| else if ( | ||
| lastChain || | ||
| lastOperand.comparisonType === NullishComparisonType.EqualNullOrUndefined || | ||
| lastOperand.comparisonType === | ||
| NullishComparisonType.NotEqualNullOrUndefined || | ||
| @@ -521,10 +616,11 @@ export function analyzeChain( | ||
| node: TSESTree.Node, | ||
| operator: TSESTree.LogicalExpression['operator'], | ||
| chain: ValidOperand[], | ||
| lastChainOperand?: LastChainOperand, | ||
| ): void { | ||
| // need at least 2 operands in a chain for it to be a chain | ||
| if ( | ||
| chain.length+ (lastChainOperand ? 1 : 0)<= 1 || | ||
| /* istanbul ignore next -- previous checks make this unreachable, but keep it for exhaustiveness check */ | ||
| operator === '??' | ||
| ) { | ||
| @@ -544,23 +640,28 @@ export function analyzeChain( | ||
| // Things like x !== null && x !== undefined have two nodes, but they are | ||
| // one logical unit here, so we'll allow them to be grouped. | ||
| let subChain: (readonly ValidOperand[] | ValidOperand)[] = []; | ||
| let lastChain: LastChainOperand | ValidOperand | undefined = undefined; | ||
| const maybeReportThenReset = ( | ||
| newChainSeed?: readonly [ValidOperand, ...ValidOperand[]], | ||
| ): void => { | ||
| if (subChain.length+ (lastChain ? 1 : 0)> 1) { | ||
| const subChainFlat = subChain.flat(); | ||
| const maybeNullishNodes = lastChain | ||
| ? subChainFlat.map(({ node }) => node) | ||
| : subChainFlat.slice(0, -1).map(({ node }) => node); | ||
| checkNullishAndReport( | ||
| context, | ||
| parserServices, | ||
| options, | ||
| maybeNullishNodes, | ||
| getReportDescriptor( | ||
| context.sourceCode, | ||
| parserServices, | ||
| node, | ||
| operator, | ||
| options, | ||
| subChainFlat, | ||
| lastChain, | ||
| ), | ||
| ); | ||
| } | ||
| @@ -578,6 +679,7 @@ export function analyzeChain( | ||
| // ^^^^^^^^^^^ newChainSeed | ||
| // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ second chain | ||
| subChain = newChainSeed ? [newChainSeed] : []; | ||
| lastChain = undefined; | ||
| }; | ||
| for (let i = 0; i < chain.length; i += 1) { | ||
| @@ -595,6 +697,35 @@ export function analyzeChain( | ||
| // ^^^^^^^ invalid OR chain logical, but still part of | ||
| // the chain for combination purposes | ||
| if (lastOperand) { | ||
| const comparisonResult = compareNodes( | ||
| lastOperand.comparedName, | ||
| operand.comparedName, | ||
| ); | ||
| switch (operand.comparisonType) { | ||
| case NullishComparisonType.StrictEqualUndefined: | ||
| case NullishComparisonType.NotStrictEqualUndefined: { | ||
| if (comparisonResult === NodeComparisonResult.Subset) { | ||
| lastChain = operand; | ||
| } | ||
| break; | ||
| } | ||
| case NullishComparisonType.StrictEqualNull: | ||
| case NullishComparisonType.NotStrictEqualNull: { | ||
Comment on lines +713 to +714 ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. When the last parameter is StrictEqualNull like
Also StrictEqualNull case foo == null | foo.bar !== null
This issue is documentedhere. But as I mentioned above, I'm still curious whether this logic is necessary at all. | ||
| if ( | ||
| comparisonResult === NodeComparisonResult.Subset && | ||
| isAlwaysTruthyOperand( | ||
| lastOperand.comparedName, | ||
| lastOperand.comparisonType, | ||
| parserServices, | ||
| ) | ||
| ) { | ||
| lastChain = operand; | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| maybeReportThenReset(); | ||
| continue; | ||
| } | ||
| @@ -624,7 +755,33 @@ export function analyzeChain( | ||
| subChain.push(currentOperand); | ||
| } | ||
| } | ||
| const lastOperand = subChain.flat().at(-1); | ||
| if (lastOperand && lastChainOperand) { | ||
| const comparisonResult = compareNodes( | ||
| lastOperand.comparedName, | ||
| lastChainOperand.comparedName, | ||
| ); | ||
| const isValidLastChainOperand = | ||
| operator === '&&' | ||
| ? isValidAndLastChainOperand | ||
| : isValidOrLastChainOperand; | ||
| if ( | ||
| comparisonResult === NodeComparisonResult.Subset && | ||
| (isAlwaysTruthyOperand( | ||
| lastOperand.comparedName, | ||
| lastOperand.comparisonType, | ||
| parserServices, | ||
| ) || | ||
| isValidLastChainOperand( | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. This function checks if the last operand is valid as outlined in the table. | ||
| lastChainOperand.comparisonValue, | ||
| lastChainOperand.comparisonType, | ||
| parserServices, | ||
| )) | ||
| ) { | ||
| lastChain = lastChainOperand; | ||
| } | ||
| } | ||
| // check the leftovers | ||
| maybeReportThenReset(); | ||
| } | ||
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.