From d2acec34643d03d68718774a884c3bb5532dfc45 Mon Sep 17 00:00:00 2001 From: yeonjuan Date: Thu, 4 Jan 2024 01:04:46 +0900 Subject: [PATCH] fix(eslint-plugin): [no-unnecessary-condition] improve checking optional callee --- .../src/rules/no-unnecessary-condition.ts | 29 +++- .../rules/no-unnecessary-condition.test.ts | 134 ++++++++++++++++++ 2 files changed, 160 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index a55fd0d3956a..6bacedf0e0aa 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -547,7 +547,7 @@ export default createRule({ // declare const foo: { bar : { baz: string } } | null // foo?.bar; // ``` - function isNullableOriginFromPrev( + function isMemberExpressionNullableOriginFromObject( node: TSESTree.MemberExpression, ): boolean { const prevType = getConstrainedTypeAtLocation(services, node.object); @@ -580,12 +580,35 @@ export default createRule({ return false; } + function isCallExpressionNullableOriginFromCallee( + node: TSESTree.CallExpression, + ): boolean { + const prevType = getConstrainedTypeAtLocation(services, node.callee); + + if (prevType.isUnion()) { + const isOwnNullable = prevType.types.some(type => { + const signatures = type.getCallSignatures(); + return signatures.some(sig => + isNullableType(sig.getReturnType(), { allowUndefined: true }), + ); + }); + return ( + !isOwnNullable && isNullableType(prevType, { allowUndefined: true }) + ); + } + + return false; + } + function isOptionableExpression(node: TSESTree.Expression): boolean { const type = getConstrainedTypeAtLocation(services, node); const isOwnNullable = node.type === AST_NODE_TYPES.MemberExpression - ? !isNullableOriginFromPrev(node) - : true; + ? !isMemberExpressionNullableOriginFromObject(node) + : node.type === AST_NODE_TYPES.CallExpression + ? !isCallExpressionNullableOriginFromCallee(node) + : true; + const possiblyVoid = isTypeFlagSet(type, ts.TypeFlags.Void); return ( isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown) || 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 89cdd1a73ed5..2b7dbbd582b1 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -774,6 +774,28 @@ function getElem(dict: Record, key: string) { typescript: '4.1', }, }, + ` +type Foo = { bar: () => number | undefined } | null; +declare const foo: Foo; +foo?.bar()?.toExponential(); + `, + ` +type Foo = (() => number | undefined) | null; +declare const foo: Foo; +foo?.()?.toExponential(); + `, + ` +type FooUndef = () => undefined; +type FooNum = () => number; +type Foo = FooUndef | FooNum | null; +declare const foo: Foo; +foo?.()?.toExponential(); + `, + ` +type Foo = { [key: string]: () => number | undefined } | null; +declare const foo: Foo; +foo?.['bar']()?.toExponential(); + `, ], invalid: [ // Ensure that it's checking in all the right places @@ -1993,6 +2015,118 @@ foo &&= null; }, ], }, + { + code: noFormat` +type Foo = { bar: () => number } | null; +declare const foo: Foo; +foo?.bar()?.toExponential(); + `, + output: noFormat` +type Foo = { bar: () => number } | null; +declare const foo: Foo; +foo?.bar().toExponential(); + `, + errors: [ + { + messageId: 'neverOptionalChain', + line: 4, + column: 11, + endLine: 4, + endColumn: 13, + }, + ], + }, + { + code: noFormat` +type Foo = { bar: null | { baz: () => { qux: number } } } | null; +declare const foo: Foo; +foo?.bar?.baz()?.qux?.toExponential(); + `, + output: noFormat` +type Foo = { bar: null | { baz: () => { qux: number } } } | null; +declare const foo: Foo; +foo?.bar?.baz().qux.toExponential(); + `, + errors: [ + { + messageId: 'neverOptionalChain', + line: 4, + column: 16, + endLine: 4, + endColumn: 18, + }, + { + messageId: 'neverOptionalChain', + line: 4, + column: 21, + endLine: 4, + endColumn: 23, + }, + ], + }, + { + code: noFormat` +type Foo = (() => number) | null; +declare const foo: Foo; +foo?.()?.toExponential(); + `, + output: noFormat` +type Foo = (() => number) | null; +declare const foo: Foo; +foo?.().toExponential(); + `, + errors: [ + { + messageId: 'neverOptionalChain', + line: 4, + column: 8, + endLine: 4, + endColumn: 10, + }, + ], + }, + { + code: noFormat` +type Foo = { [key: string]: () => number } | null; +declare const foo: Foo; +foo?.['bar']()?.toExponential(); + `, + output: noFormat` +type Foo = { [key: string]: () => number } | null; +declare const foo: Foo; +foo?.['bar']().toExponential(); + `, + errors: [ + { + messageId: 'neverOptionalChain', + line: 4, + column: 15, + endLine: 4, + endColumn: 17, + }, + ], + }, + { + code: noFormat` +type Foo = { [key: string]: () => number } | null; +declare const foo: Foo; +foo?.['bar']?.()?.toExponential(); + `, + output: noFormat` +type Foo = { [key: string]: () => number } | null; +declare const foo: Foo; +foo?.['bar']?.().toExponential(); + `, + errors: [ + { + messageId: 'neverOptionalChain', + line: 4, + column: 17, + endLine: 4, + endColumn: 19, + }, + ], + }, // "branded" types unnecessaryConditionTest('"" & {}', 'alwaysFalsy'), pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy