Skip to content

Commit 3241e46

Browse files
authored
fix(eslint-plugin): [no-unnecessary-condition] don't report on unnecessary optional array index access when noUncheckedIndexedAccess is enabled (typescript-eslint#10961)
* initial implementation * add tests * cover missing edge cases
1 parent 807f5ca commit 3241e46

File tree

2 files changed

+158
-8
lines changed

2 files changed

+158
-8
lines changed

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ export default createRule<Options, MessageId>({
357357
// Since typescript array index signature types don't represent the
358358
// possibility of out-of-bounds access, if we're indexing into an array
359359
// just skip the check, to avoid false positives
360-
if (isArrayIndexExpression(expression)) {
360+
if (!isNoUncheckedIndexedAccess && isArrayIndexExpression(expression)) {
361361
return;
362362
}
363363

@@ -424,12 +424,13 @@ export default createRule<Options, MessageId>({
424424
// possibility of out-of-bounds access, if we're indexing into an array
425425
// just skip the check, to avoid false positives
426426
if (
427-
!isArrayIndexExpression(node) &&
428-
!(
429-
node.type === AST_NODE_TYPES.ChainExpression &&
430-
node.expression.type !== AST_NODE_TYPES.TSNonNullExpression &&
431-
optionChainContainsOptionArrayIndex(node.expression)
432-
)
427+
isNoUncheckedIndexedAccess ||
428+
(!isArrayIndexExpression(node) &&
429+
!(
430+
node.type === AST_NODE_TYPES.ChainExpression &&
431+
node.expression.type !== AST_NODE_TYPES.TSNonNullExpression &&
432+
optionChainContainsOptionArrayIndex(node.expression)
433+
))
433434
) {
434435
messageId = 'neverNullish';
435436
}
@@ -835,7 +836,10 @@ export default createRule<Options, MessageId>({
835836
// Since typescript array index signature types don't represent the
836837
// possibility of out-of-bounds access, if we're indexing into an array
837838
// just skip the check, to avoid false positives
838-
if (optionChainContainsOptionArrayIndex(node)) {
839+
if (
840+
!isNoUncheckedIndexedAccess &&
841+
optionChainContainsOptionArrayIndex(node)
842+
) {
839843
return;
840844
}
841845

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

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,21 @@ const tuple = ['foo'] as const;
399399
declare const n: number;
400400
tuple[n]?.toUpperCase();
401401
`,
402+
{
403+
code: `
404+
declare const arr: Array<{ value: string } & (() => void)>;
405+
if (arr[42]?.value) {
406+
}
407+
arr[41]?.();
408+
`,
409+
languageOptions: {
410+
parserOptions: {
411+
project: './tsconfig.noUncheckedIndexedAccess.json',
412+
projectService: false,
413+
tsconfigRootDir: getFixturesRootDir(),
414+
},
415+
},
416+
},
402417
`
403418
if (arr?.[42]) {
404419
}
@@ -417,6 +432,23 @@ declare const foo: TupleA | TupleB;
417432
declare const index: number;
418433
foo[index]?.toString();
419434
`,
435+
{
436+
code: `
437+
type TupleA = [string, number];
438+
type TupleB = [string, number];
439+
440+
declare const foo: TupleA | TupleB;
441+
declare const index: number;
442+
foo[index]?.toString();
443+
`,
444+
languageOptions: {
445+
parserOptions: {
446+
project: './tsconfig.noUncheckedIndexedAccess.json',
447+
projectService: false,
448+
tsconfigRootDir: getFixturesRootDir(),
449+
},
450+
},
451+
},
420452
`
421453
declare const returnsArr: undefined | (() => string[]);
422454
if (returnsArr?.()[42]) {
@@ -3272,5 +3304,119 @@ declare const t: T;
32723304
t.a.a.a.value;
32733305
`,
32743306
},
3307+
{
3308+
code: `
3309+
declare const test: Array<{ a?: string }>;
3310+
3311+
if (test[0]?.a) {
3312+
test[0]?.a;
3313+
}
3314+
`,
3315+
errors: [
3316+
{
3317+
column: 10,
3318+
endColumn: 12,
3319+
endLine: 5,
3320+
line: 5,
3321+
messageId: 'neverOptionalChain',
3322+
},
3323+
],
3324+
languageOptions: {
3325+
parserOptions: {
3326+
project: './tsconfig.noUncheckedIndexedAccess.json',
3327+
projectService: false,
3328+
tsconfigRootDir: getFixturesRootDir(),
3329+
},
3330+
},
3331+
output: `
3332+
declare const test: Array<{ a?: string }>;
3333+
3334+
if (test[0]?.a) {
3335+
test[0].a;
3336+
}
3337+
`,
3338+
},
3339+
{
3340+
code: `
3341+
declare const arr2: Array<{ x: { y: { z: object } } }>;
3342+
arr2[42]?.x?.y?.z;
3343+
`,
3344+
errors: [
3345+
{
3346+
column: 12,
3347+
endColumn: 14,
3348+
endLine: 3,
3349+
line: 3,
3350+
messageId: 'neverOptionalChain',
3351+
},
3352+
{
3353+
column: 15,
3354+
endColumn: 17,
3355+
endLine: 3,
3356+
line: 3,
3357+
messageId: 'neverOptionalChain',
3358+
},
3359+
],
3360+
languageOptions: {
3361+
parserOptions: {
3362+
project: './tsconfig.noUncheckedIndexedAccess.json',
3363+
projectService: false,
3364+
tsconfigRootDir: getFixturesRootDir(),
3365+
},
3366+
},
3367+
output: `
3368+
declare const arr2: Array<{ x: { y: { z: object } } }>;
3369+
arr2[42]?.x.y.z;
3370+
`,
3371+
},
3372+
{
3373+
code: `
3374+
declare const arr: string[];
3375+
3376+
if (arr[0]) {
3377+
arr[0] ?? 'foo';
3378+
}
3379+
`,
3380+
errors: [
3381+
{
3382+
column: 3,
3383+
endColumn: 9,
3384+
endLine: 5,
3385+
line: 5,
3386+
messageId: 'neverNullish',
3387+
},
3388+
],
3389+
languageOptions: {
3390+
parserOptions: {
3391+
project: './tsconfig.noUncheckedIndexedAccess.json',
3392+
projectService: false,
3393+
tsconfigRootDir: getFixturesRootDir(),
3394+
},
3395+
},
3396+
},
3397+
{
3398+
code: `
3399+
declare const arr: object[];
3400+
3401+
if (arr[42] && arr[42]) {
3402+
}
3403+
`,
3404+
errors: [
3405+
{
3406+
column: 16,
3407+
endColumn: 23,
3408+
endLine: 4,
3409+
line: 4,
3410+
messageId: 'alwaysTruthy',
3411+
},
3412+
],
3413+
languageOptions: {
3414+
parserOptions: {
3415+
project: './tsconfig.noUncheckedIndexedAccess.json',
3416+
projectService: false,
3417+
tsconfigRootDir: getFixturesRootDir(),
3418+
},
3419+
},
3420+
},
32753421
],
32763422
});

0 commit comments

Comments
 (0)