diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 19e734179127..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'; } @@ -835,7 +836,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..d989188f7c36 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]) { @@ -3272,5 +3304,119 @@ 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; +} + `, + }, + { + 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; + `, + }, + { + 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(), + }, + }, + }, ], });