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