diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 40787600143..12a42398b3e 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 a3f26300b2c..7c31bf41957 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -772,6 +772,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 @@ -1991,6 +2013,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'),