Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
24 commits
Select commitHold shift + click to select a range
cfdd536
test: add default test
mdm317Aug 26, 2025
33e6c71
feat: comparison operators at the end of optional chains
mdm317Aug 26, 2025
6bf2f64
fix : comparison operators with logical operators Ensure comparison o…
mdm317Aug 26, 2025
3aa3b04
fix : suggestion
mdm317Aug 26, 2025
51d956f
fix : != null is last chain
mdm317Aug 26, 2025
8c3de3a
text: change basecases
mdm317Aug 26, 2025
5987dfc
test: change !== undefined base cases
mdm317Aug 26, 2025
bd5d289
test: when !== undefined is last chain
mdm317Aug 26, 2025
15b7fc8
test: add some union case
mdm317Aug 26, 2025
7ffa803
test: add base case in `||` operator
mdm317Aug 26, 2025
df6809a
fix : test case
mdm317Aug 26, 2025
165e914
feat : or chain
mdm317Aug 26, 2025
b7d2e02
refactoring: change funtion name
mdm317Sep 3, 2025
2adc3da
Merge branch 'main' into feat/prefer-optional-chain-with-comparison-o…
mdm317Sep 4, 2025
7fe1f33
lint
mdm317Sep 4, 2025
90c13fa
lint
mdm317Sep 4, 2025
609d1aa
chore: new readme shot
mdm317Sep 4, 2025
9aa435a
Refactor
mdm317Sep 4, 2025
befd7a0
test: add missing case, and chain when the last chain is != operand
mdm317Sep 5, 2025
c59f77f
test: yoda case when `& chain`
mdm317Sep 5, 2025
aa793f2
add missing logic
mdm317Sep 5, 2025
2a5798b
test : add missing case
mdm317Sep 5, 2025
f71611f
add missing case
mdm317Sep 5, 2025
f8e07cf
Merge branch 'main'
JoshuaKGoldbergOct 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletionspackages/eslint-plugin/src/rules/no-base-to-string.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

const declarations: ts.Declaration[] | undefined

When the new logic was applied, this code
declarations == null || declarations.length !== 1
was changed to
declarations?.length !== 1

But I think the original code is more readable and intuitive.
For cases likefoo == null || <equaliy check>, should we avoid optional chaining?
What do you think?
example new playground

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

If the choices are these:

  • Original:if (declations == null || declations.length !== 1) {
  • Updated:if (declations?.length !== 1) {

Personal anecodtal thoughts: I find the updated form more readable. I think when?. was new I would probably have preferred the original for being more familiar. But at this point I'm pretty comfortable with?. and??, and it feels natural to me.

mdm317 reacted with thumbs up emoji
// If there are multiple declarations, at least one of them must not be
// the default object toString.
Expand Down
2 changes: 1 addition & 1 deletionpackages/eslint-plugin/src/rules/prefer-includes.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,7 +39,7 @@ export default createRule({

function isNumber(node: TSESTree.Node, value: number): boolean {
const evaluated = getStaticValue(node, globalScope);
return evaluated != null && evaluated.value === value;
return evaluated?.value === value;
}

function isPositiveCheck(node: TSESTree.BinaryExpression): boolean {
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,10 @@ import type {
} from '@typescript-eslint/utils/ts-eslint';

import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import { unionConstituents } from 'ts-api-utils';
import {isFalsyType,unionConstituents } from 'ts-api-utils';
import * as ts from 'typescript';

import type { ValidOperand } from './gatherLogicalOperands';
import type {LastChainOperand,ValidOperand } from './gatherLogicalOperands';
import type {
PreferOptionalChainMessageIds,
PreferOptionalChainOptions,
Expand All@@ -31,7 +31,7 @@ import {
} from '../../util';
import { checkNullishAndReport } from './checkNullishAndReport';
import { compareNodes, NodeComparisonResult } from './compareNodes';
import { NullishComparisonType } from './gatherLogicalOperands';
import {ComparisonType,NullishComparisonType } from './gatherLogicalOperands';

function includesType(
parserServices: ParserServicesWithTypeInformation,
Expand All@@ -48,6 +48,109 @@ function includesType(
return false;
}

function isAlwaysTruthyOperand(
Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I'm not convinced this function is necessary.
This function checks if the previous operand is always true:

  • foo != null && foo.bar == somevaluefoo?.bar == somevalue (same result when foo is not nullish)
  • foo && foo.bar == somevaluefoo?.bar == somevalue (same result when foo is truthy)

But if we knowfoo is not nullish, we can simply writefoo.bar == somevalue without optional chaining at all.

Copy link
Member

@JoshuaKGoldbergJoshuaKGoldbergOct 3, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Removing it fails some unit tests (that have legitimate cases, I believe). E.g.:

declareconstfoo:{bar:string}|null;foo!==null&&foo.bar!==null;

mdm317 reacted with thumbs up emoji
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.
Expand All@@ -65,18 +168,7 @@ const analyzeAndChainOperand: OperandAnalyzer = (
chain,
) => {
switch (operand.comparisonType) {
case NullishComparisonType.Boolean: {
Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This logic moved to line 700.

JoshuaKGoldberg reacted with thumbs up emoji

Choose a reason for hiding this comment

The 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. 🙂

mdm317 reacted with thumbs up emoji
const nextOperand = chain.at(index + 1);
if (
nextOperand?.comparisonType ===
NullishComparisonType.NotStrictEqualNull &&
operand.comparedName.type === AST_NODE_TYPES.Identifier
) {
return null;
}
return [operand];
}

case NullishComparisonType.Boolean:
case NullishComparisonType.NotEqualNullOrUndefined:
return [operand];

Expand All@@ -92,7 +184,8 @@ const analyzeAndChainOperand: OperandAnalyzer = (
return [operand, nextOperand];
}
if (
includesType(
nextOperand &&
!includesType(
parserServices,
operand.comparedName,
ts.TypeFlags.Undefined,
Expand All@@ -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
returnnull;
return[operand];
}

return [operand];
return null;
}

case NullishComparisonType.NotStrictEqualUndefined: {
Expand DownExpand Up@@ -156,6 +248,7 @@ const analyzeOrChainOperand: OperandAnalyzer = (
) {
return [operand, nextOperand];
}

if (
includesType(
parserServices,
Expand All@@ -168,7 +261,6 @@ const analyzeOrChainOperand: OperandAnalyzer = (
// optional chain would change the runtime behavior of the expression
return null;
}

return [operand];
}

Expand DownExpand Up@@ -207,7 +299,7 @@ const analyzeOrChainOperand: OperandAnalyzer = (
* @returns The range to report.
*/
function getReportRange(
chain:ValidOperand[],
chain:{ node: TSESTree.Expression }[],
boundary: TSESTree.Range,
sourceCode: SourceCode,
): TSESTree.Range {
Expand DownExpand Up@@ -247,8 +339,10 @@ function getReportDescriptor(
node: TSESTree.Node,
operator: '&&' | '||',
options: PreferOptionalChainOptions,
chain: ValidOperand[],
subChain: ValidOperand[],
lastChain: (LastChainOperand | ValidOperand) | undefined,
): ReportDescriptor<PreferOptionalChainMessageIds> {
const chain = lastChain ? [...subChain, lastChain] : subChain;
const lastOperand = chain[chain.length - 1];

let useSuggestionFixer: boolean;
Expand All@@ -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 ||
Expand DownExpand Up@@ -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 <= 1 ||
chain.length+ (lastChainOperand ? 1 : 0)<= 1 ||
/* istanbul ignore next -- previous checks make this unreachable, but keep it for exhaustiveness check */
operator === '??'
) {
Expand All@@ -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 > 1) {
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,
subChainFlat.slice(0, -1).map(({ node }) => node),
maybeNullishNodes,
getReportDescriptor(
context.sourceCode,
parserServices,
node,
operator,
options,
subChainFlat,
lastChain,
),
);
}
Expand All@@ -578,6 +679,7 @@ export function analyzeChain(
// ^^^^^^^^^^^ newChainSeed
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ second chain
subChain = newChainSeed ? [newChainSeed] : [];
lastChain = undefined;
};

for (let i = 0; i < chain.length; i += 1) {
Expand All@@ -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
Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

When the last parameter is StrictEqualNull likefoo && foo.bar !== null, we can't convert to optional chaining because:

  • foo && foo.bar !== null returnsfalse when foo is nullish
  • foo?.bar !== null returnstrue when foo is nullish

Also StrictEqualNull case foo == null | foo.bar !== null

  • foo == null || foo.bar !== null returnstrue when foo is nullish
  • foo?.bar !== null returnsfalse when foo is nullish

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;
}
Expand DownExpand Up@@ -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(
Copy link
ContributorAuthor

Choose a reason for hiding this comment

The 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.

JoshuaKGoldberg reacted with thumbs up emoji
lastChainOperand.comparisonValue,
lastChainOperand.comparisonType,
parserServices,
))
) {
lastChain = lastChainOperand;
}
}
// check the leftovers
maybeReportThenReset();
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp