From 06db97e960e30badd2f82911d406afa36dd4c2c5 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sun, 16 Mar 2025 20:16:50 +0200 Subject: [PATCH 1/3] initial implementation --- .../src/rules/no-unnecessary-condition.ts | 5 ++- .../rules/no-unnecessary-condition.test.ts | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 19e734179127..ed1ecfd155be 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -835,7 +835,10 @@ export default createRule({ // Since typescript array index signature types don't represent the // possibility of out-of-bounds access, if we're indexing into an array // just skip the check, to avoid false positives - if (optionChainContainsOptionArrayIndex(node)) { + if ( + !isNoUncheckedIndexedAccess && + optionChainContainsOptionArrayIndex(node) + ) { return; } diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index f9936e5cbb46..e1aa84c661e8 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -3272,5 +3272,37 @@ declare const t: T; t.a.a.a.value; `, }, + { + code: ` +declare const test: Array<{ a?: string }>; + +if (test[0]?.a) { + test[0]?.a; +} + `, + errors: [ + { + column: 10, + endColumn: 12, + endLine: 5, + line: 5, + messageId: 'neverOptionalChain', + }, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + projectService: false, + tsconfigRootDir: getFixturesRootDir(), + }, + }, + output: ` +declare const test: Array<{ a?: string }>; + +if (test[0]?.a) { + test[0].a; +} + `, + }, ], }); From a03ba8e53d31e833d5587d9dba48e9222ddedf64 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sun, 16 Mar 2025 20:37:18 +0200 Subject: [PATCH 2/3] add tests --- .../rules/no-unnecessary-condition.test.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index e1aa84c661e8..392b0ff742e4 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -399,6 +399,21 @@ const tuple = ['foo'] as const; declare const n: number; tuple[n]?.toUpperCase(); `, + { + code: ` +declare const arr: Array<{ value: string } & (() => void)>; +if (arr[42]?.value) { +} +arr[41]?.(); + `, + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + projectService: false, + tsconfigRootDir: getFixturesRootDir(), + }, + }, + }, ` if (arr?.[42]) { } @@ -417,6 +432,23 @@ declare const foo: TupleA | TupleB; declare const index: number; foo[index]?.toString(); `, + { + code: ` +type TupleA = [string, number]; +type TupleB = [string, number]; + +declare const foo: TupleA | TupleB; +declare const index: number; +foo[index]?.toString(); + `, + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + projectService: false, + tsconfigRootDir: getFixturesRootDir(), + }, + }, + }, ` declare const returnsArr: undefined | (() => string[]); if (returnsArr?.()[42]) { @@ -3304,5 +3336,38 @@ if (test[0]?.a) { } `, }, + { + code: ` +declare const arr2: Array<{ x: { y: { z: object } } }>; +arr2[42]?.x?.y?.z; + `, + errors: [ + { + column: 12, + endColumn: 14, + endLine: 3, + line: 3, + messageId: 'neverOptionalChain', + }, + { + column: 15, + endColumn: 17, + endLine: 3, + line: 3, + messageId: 'neverOptionalChain', + }, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + projectService: false, + tsconfigRootDir: getFixturesRootDir(), + }, + }, + output: ` +declare const arr2: Array<{ x: { y: { z: object } } }>; +arr2[42]?.x.y.z; + `, + }, ], }); From e3a479c829964cea4b59dff74ada659d6e1415f0 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sun, 23 Mar 2025 21:28:58 +0200 Subject: [PATCH 3/3] cover missing edge cases --- .../src/rules/no-unnecessary-condition.ts | 15 +++--- .../rules/no-unnecessary-condition.test.ts | 49 +++++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index ed1ecfd155be..88c5e42f0b53 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -357,7 +357,7 @@ export default createRule({ // Since typescript array index signature types don't represent the // possibility of out-of-bounds access, if we're indexing into an array // just skip the check, to avoid false positives - if (isArrayIndexExpression(expression)) { + if (!isNoUncheckedIndexedAccess && isArrayIndexExpression(expression)) { return; } @@ -424,12 +424,13 @@ export default createRule({ // possibility of out-of-bounds access, if we're indexing into an array // just skip the check, to avoid false positives if ( - !isArrayIndexExpression(node) && - !( - node.type === AST_NODE_TYPES.ChainExpression && - node.expression.type !== AST_NODE_TYPES.TSNonNullExpression && - optionChainContainsOptionArrayIndex(node.expression) - ) + isNoUncheckedIndexedAccess || + (!isArrayIndexExpression(node) && + !( + node.type === AST_NODE_TYPES.ChainExpression && + node.expression.type !== AST_NODE_TYPES.TSNonNullExpression && + optionChainContainsOptionArrayIndex(node.expression) + )) ) { messageId = 'neverNullish'; } diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index 392b0ff742e4..d989188f7c36 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -3369,5 +3369,54 @@ declare const arr2: Array<{ x: { y: { z: object } } }>; arr2[42]?.x.y.z; `, }, + { + code: ` +declare const arr: string[]; + +if (arr[0]) { + arr[0] ?? 'foo'; +} + `, + errors: [ + { + column: 3, + endColumn: 9, + endLine: 5, + line: 5, + messageId: 'neverNullish', + }, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + projectService: false, + tsconfigRootDir: getFixturesRootDir(), + }, + }, + }, + { + code: ` +declare const arr: object[]; + +if (arr[42] && arr[42]) { +} + `, + errors: [ + { + column: 16, + endColumn: 23, + endLine: 4, + line: 4, + messageId: 'alwaysTruthy', + }, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + projectService: false, + tsconfigRootDir: getFixturesRootDir(), + }, + }, + }, ], });