Skip to content

Commit bdb8f0b

Browse files
authoredSep 20, 2021
fix(eslint-plugin): false-positive/negative with array index in no-unnecessary-condition (#3805)
* fix: false negative with always nullish after array index access * fix: false positive with nullish + optional array index * fix: false negative with optional chain after array index
1 parent 4e99961 commit bdb8f0b

File tree

2 files changed

+50
-11
lines changed

2 files changed

+50
-11
lines changed
 

‎packages/eslint-plugin/src/rules/no-unnecessary-condition.ts

+17-11
Original file line numberDiff line numberDiff line change
@@ -261,12 +261,6 @@ export default createRule<Options, MessageId>({
261261
}
262262

263263
function checkNodeForNullish(node: TSESTree.Expression): void {
264-
// Since typescript array index signature types don't represent the
265-
// possibility of out-of-bounds access, if we're indexing into an array
266-
// just skip the check, to avoid false positives
267-
if (isArrayIndexExpression(node)) {
268-
return;
269-
}
270264
const type = getNodeType(node);
271265
// Conditional is always necessary if it involves `any` or `unknown`
272266
if (isTypeAnyType(type) || isTypeUnknownType(type)) {
@@ -277,7 +271,19 @@ export default createRule<Options, MessageId>({
277271
if (isTypeFlagSet(type, ts.TypeFlags.Never)) {
278272
messageId = 'never';
279273
} else if (!isPossiblyNullish(type)) {
280-
messageId = 'neverNullish';
274+
// Since typescript array index signature types don't represent the
275+
// possibility of out-of-bounds access, if we're indexing into an array
276+
// just skip the check, to avoid false positives
277+
if (
278+
!isArrayIndexExpression(node) &&
279+
!(
280+
node.type === AST_NODE_TYPES.ChainExpression &&
281+
node.expression.type !== AST_NODE_TYPES.TSNonNullExpression &&
282+
optionChainContainsOptionArrayIndex(node.expression)
283+
)
284+
) {
285+
messageId = 'neverNullish';
286+
}
281287
} else if (isAlwaysNullish(type)) {
282288
messageId = 'alwaysNullish';
283289
}
@@ -477,19 +483,19 @@ export default createRule<Options, MessageId>({
477483
// ?.x // type is {y: "z"}
478484
// ?.y // This access is considered "unnecessary" according to the types
479485
// ```
480-
function optionChainContainsArrayIndex(
486+
function optionChainContainsOptionArrayIndex(
481487
node: TSESTree.MemberExpression | TSESTree.CallExpression,
482488
): boolean {
483489
const lhsNode =
484490
node.type === AST_NODE_TYPES.CallExpression ? node.callee : node.object;
485-
if (isArrayIndexExpression(lhsNode)) {
491+
if (node.optional && isArrayIndexExpression(lhsNode)) {
486492
return true;
487493
}
488494
if (
489495
lhsNode.type === AST_NODE_TYPES.MemberExpression ||
490496
lhsNode.type === AST_NODE_TYPES.CallExpression
491497
) {
492-
return optionChainContainsArrayIndex(lhsNode);
498+
return optionChainContainsOptionArrayIndex(lhsNode);
493499
}
494500
return false;
495501
}
@@ -584,7 +590,7 @@ export default createRule<Options, MessageId>({
584590
// Since typescript array index signature types don't represent the
585591
// possibility of out-of-bounds access, if we're indexing into an array
586592
// just skip the check, to avoid false positives
587-
if (optionChainContainsArrayIndex(node)) {
593+
if (optionChainContainsOptionArrayIndex(node)) {
588594
return;
589595
}
590596

‎packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts

+33
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,11 @@ returnsArr?.()[42]?.toUpperCase();
315315
`
316316
declare const arr: string[][];
317317
arr[x] ?? [];
318+
`,
319+
// nullish + optional array index
320+
`
321+
declare const arr: { foo: number }[];
322+
const bar = arr[42]?.foo ?? 0;
318323
`,
319324
// Doesn't check the right-hand side of a logical expression
320325
// in a non-conditional context
@@ -751,6 +756,15 @@ function test(a: string) {
751756
code: `
752757
function test(a: string | false) {
753758
return a ?? 'default';
759+
}
760+
`,
761+
errors: [ruleError(3, 10, 'neverNullish')],
762+
},
763+
// nullish + array index without optional chaining
764+
{
765+
code: `
766+
function test(a: { foo: string }[]) {
767+
return a[0].foo ?? 'default';
754768
}
755769
`,
756770
errors: [ruleError(3, 10, 'neverNullish')],
@@ -765,6 +779,14 @@ function test(a: null) {
765779
},
766780
{
767781
code: `
782+
function test(a: null[]) {
783+
return a[0] ?? 'default';
784+
}
785+
`,
786+
errors: [ruleError(3, 10, 'alwaysNullish')],
787+
},
788+
{
789+
code: `
768790
function test(a: never) {
769791
return a ?? 'default';
770792
}
@@ -1315,6 +1337,17 @@ foo?.fooOrBar.baz?.qux;
13151337
},
13161338
{
13171339
code: `
1340+
declare const x: { a: { b: number } }[];
1341+
x[0].a?.b;
1342+
`,
1343+
output: `
1344+
declare const x: { a: { b: number } }[];
1345+
x[0].a.b;
1346+
`,
1347+
errors: [ruleError(3, 7, 'neverOptionalChain')],
1348+
},
1349+
{
1350+
code: `
13181351
type Foo = { [key: string]: string; foo: 'foo'; bar: 'bar' } | null;
13191352
type Key = 'bar' | 'foo';
13201353
declare const foo: Foo;

0 commit comments

Comments
 (0)
Please sign in to comment.