From 059dfb8aa37f0676044e263fd09b2bf132ed6636 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Tue, 17 Dec 2024 21:05:17 +0900 Subject: [PATCH 1/3] fix(eslint-plugin): [no-unnecessary-condition] handle index signature --- .../src/rules/no-unnecessary-condition.ts | 4 +- .../rules/no-unnecessary-condition.test.ts | 53 ++++++------------- 2 files changed, 17 insertions(+), 40 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index c9660416a81b..a5f0b346ad8a 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -757,9 +757,7 @@ export default createRule({ const indexInfo = checker.getIndexInfosOfType(type); return indexInfo.some( - info => - getTypeName(checker, info.keyType) === 'string' && - isNullableType(info.type), + info => getTypeName(checker, info.keyType) === 'string', ); }); return !isOwnNullable && isNullableType(prevType); 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 de69b94afac3..e88ae919dc32 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -1010,6 +1010,22 @@ declare const t: T; t.a.a.a.value; t.A?.A?.A?.VALUE; `, + ` +type Foo = { + key?: Record; +}; +declare const foo: Foo; +foo.key?.value?.length; + `, + ` +type Foo = { + key?: { + [key: string]: () => void; + }; +}; +declare const foo: Foo; +foo.key?.value?.(); + `, ], invalid: [ @@ -2915,43 +2931,6 @@ isString('fa' + 'lafel'); ), { code: ` -type A = { - [name in Lowercase]?: { - [name in Lowercase]: { - a: 1; - }; - }; -}; - -declare const a: A; - -a.a?.a?.a; - `, - errors: [ - { - column: 7, - endColumn: 9, - endLine: 12, - line: 12, - messageId: 'neverOptionalChain', - }, - ], - output: ` -type A = { - [name in Lowercase]?: { - [name in Lowercase]: { - a: 1; - }; - }; -}; - -declare const a: A; - -a.a?.a.a; - `, - }, - { - code: ` interface T { [name: Lowercase]: { [name: Lowercase]: { From 572d02f7def184b2abadeaa304582d6282959da3 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Wed, 18 Dec 2024 02:18:40 +0900 Subject: [PATCH 2/3] fix --- .../src/rules/no-unnecessary-condition.ts | 16 +++- .../rules/no-unnecessary-condition.test.ts | 76 +++++++++++++++++-- 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index a5f0b346ad8a..19e292b0db8c 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -273,6 +273,10 @@ export default createRule({ compilerOptions, 'strictNullChecks', ); + const isNoUncheckedIndexedAccess = tsutils.isCompilerOptionEnabled( + compilerOptions, + 'noUncheckedIndexedAccess', + ); if ( !isStrictNullChecks && @@ -756,9 +760,15 @@ export default createRule({ } const indexInfo = checker.getIndexInfosOfType(type); - return indexInfo.some( - info => getTypeName(checker, info.keyType) === 'string', - ); + return indexInfo.some(info => { + const isStringTypeName = + getTypeName(checker, info.keyType) === 'string'; + + return ( + isStringTypeName && + (isNoUncheckedIndexedAccess || isNullableType(info.type)) + ); + }); }); return !isOwnNullable && isNullableType(prevType); } 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 e88ae919dc32..3905da73296a 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -27,6 +27,11 @@ const optionsWithExactOptionalPropertyTypes = { tsconfigRootDir: rootPath, }; +const optionsWithNoUncheckedIndexedAccess = { + project: './tsconfig.noUncheckedIndexedAccess.json', + tsconfigRootDir: rootPath, +}; + const necessaryConditionTest = (condition: string): string => ` declare const b1: ${condition}; declare const b2: boolean; @@ -1010,14 +1015,18 @@ declare const t: T; t.a.a.a.value; t.A?.A?.A?.VALUE; `, - ` + { + code: ` type Foo = { - key?: Record; + key?: Record; }; declare const foo: Foo; -foo.key?.value?.length; - `, - ` +foo.key?.someKey?.key; + `, + languageOptions: { parserOptions: optionsWithNoUncheckedIndexedAccess }, + }, + { + code: ` type Foo = { key?: { [key: string]: () => void; @@ -1025,7 +1034,25 @@ type Foo = { }; declare const foo: Foo; foo.key?.value?.(); - `, + `, + languageOptions: { parserOptions: optionsWithNoUncheckedIndexedAccess }, + }, + { + code: ` +type A = { + [name in Lowercase]?: { + [name in Lowercase]: { + a: 1; + }; + }; +}; + +declare const a: A; + +a.a?.a?.a; + `, + languageOptions: { parserOptions: optionsWithNoUncheckedIndexedAccess }, + }, ], invalid: [ @@ -2931,6 +2958,43 @@ isString('fa' + 'lafel'); ), { code: ` +type A = { + [name in Lowercase]?: { + [name in Lowercase]: { + a: 1; + }; + }; +}; + +declare const a: A; + +a.a?.a?.a; + `, + errors: [ + { + column: 7, + endColumn: 9, + endLine: 12, + line: 12, + messageId: 'neverOptionalChain', + }, + ], + output: ` +type A = { + [name in Lowercase]?: { + [name in Lowercase]: { + a: 1; + }; + }; +}; + +declare const a: A; + +a.a?.a.a; + `, + }, + { + code: ` interface T { [name: Lowercase]: { [name: Lowercase]: { From 31f10b067b19b9f5ba5c69a90568c0f1471a6a92 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Wed, 18 Dec 2024 02:38:55 +0900 Subject: [PATCH 3/3] refactor --- .../rules/no-unnecessary-condition.test.ts | 103 +++++++++--------- 1 file changed, 49 insertions(+), 54 deletions(-) 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 3905da73296a..f179b7c1d8fb 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -29,7 +29,8 @@ const optionsWithExactOptionalPropertyTypes = { const optionsWithNoUncheckedIndexedAccess = { project: './tsconfig.noUncheckedIndexedAccess.json', - tsconfigRootDir: rootPath, + projectService: false, + tsconfigRootDir: getFixturesRootDir(), }; const necessaryConditionTest = (condition: string): string => ` @@ -596,11 +597,7 @@ const key = '1' as BrandedKey; foo?.[key]?.trim(); `, languageOptions: { - parserOptions: { - project: './tsconfig.noUncheckedIndexedAccess.json', - projectService: false, - tsconfigRootDir: getFixturesRootDir(), - }, + parserOptions: optionsWithNoUncheckedIndexedAccess, }, }, { @@ -654,11 +651,7 @@ function Foo(outer: Outer, key: Foo): number | undefined { } `, languageOptions: { - parserOptions: { - project: './tsconfig.noUncheckedIndexedAccess.json', - projectService: false, - tsconfigRootDir: getFixturesRootDir(), - }, + parserOptions: optionsWithNoUncheckedIndexedAccess, }, }, { @@ -671,11 +664,51 @@ declare const key: Key; foo?.[key]?.trim(); `, languageOptions: { - parserOptions: { - project: './tsconfig.noUncheckedIndexedAccess.json', - projectService: false, - tsconfigRootDir: getFixturesRootDir(), - }, + parserOptions: optionsWithNoUncheckedIndexedAccess, + }, + }, + { + code: ` +type Foo = { + key?: Record; +}; +declare const foo: Foo; +foo.key?.someKey?.key; + `, + languageOptions: { + parserOptions: optionsWithNoUncheckedIndexedAccess, + }, + }, + { + code: ` +type Foo = { + key?: { + [key: string]: () => void; + }; +}; +declare const foo: Foo; +foo.key?.value?.(); + `, + languageOptions: { + parserOptions: optionsWithNoUncheckedIndexedAccess, + }, + }, + { + code: ` +type A = { + [name in Lowercase]?: { + [name in Lowercase]: { + a: 1; + }; + }; +}; + +declare const a: A; + +a.a?.a?.a; + `, + languageOptions: { + parserOptions: optionsWithNoUncheckedIndexedAccess, }, }, ` @@ -1015,44 +1048,6 @@ declare const t: T; t.a.a.a.value; t.A?.A?.A?.VALUE; `, - { - code: ` -type Foo = { - key?: Record; -}; -declare const foo: Foo; -foo.key?.someKey?.key; - `, - languageOptions: { parserOptions: optionsWithNoUncheckedIndexedAccess }, - }, - { - code: ` -type Foo = { - key?: { - [key: string]: () => void; - }; -}; -declare const foo: Foo; -foo.key?.value?.(); - `, - languageOptions: { parserOptions: optionsWithNoUncheckedIndexedAccess }, - }, - { - code: ` -type A = { - [name in Lowercase]?: { - [name in Lowercase]: { - a: 1; - }; - }; -}; - -declare const a: A; - -a.a?.a?.a; - `, - languageOptions: { parserOptions: optionsWithNoUncheckedIndexedAccess }, - }, ], invalid: [