diff --git a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx index bc8ae0f33b6e..f4fd23fec126 100644 --- a/packages/eslint-plugin/docs/rules/no-misused-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-misused-promises.mdx @@ -134,6 +134,8 @@ if (promise) { const val = promise ? 123 : 456; +[1, 2, 3].filter(() => promise); + while (promise) { // Do something } @@ -152,6 +154,9 @@ if (await promise) { const val = (await promise) ? 123 : 456; +const returnVal = await promise; +[1, 2, 3].filter(() => returnVal); + while (await promise) { // Do something } diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index c6e0978f1cb0..9edd31141e0a 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -6,6 +6,7 @@ import * as ts from 'typescript'; import { createRule, getParserServices, + isArrayMethodCallWithPredicate, isFunction, isRestParameterDeclaration, nullThrows, @@ -31,6 +32,7 @@ interface ChecksVoidReturnOptions { type MessageId = | 'conditional' + | 'predicate' | 'spread' | 'voidReturnArgument' | 'voidReturnAttribute' @@ -91,6 +93,7 @@ export default createRule({ voidReturnVariable: 'Promise-returning function provided to variable where a void return was expected.', conditional: 'Expected non-Promise value in a boolean conditional.', + predicate: 'Expected a non-Promise value to be returned.', spread: 'Expected a non-Promise value to be spreaded in an object.', }, schema: [ @@ -175,6 +178,7 @@ export default createRule({ checkConditional(node.argument, true); }, WhileStatement: checkTestConditional, + 'CallExpression > MemberExpression': checkArrayPredicates, }; checksVoidReturn = parseChecksVoidReturn(checksVoidReturn); @@ -322,6 +326,25 @@ export default createRule({ } } + function checkArrayPredicates(node: TSESTree.MemberExpression): void { + const parent = node.parent; + if (parent.type === AST_NODE_TYPES.CallExpression) { + const callback = parent.arguments.at(0); + if ( + callback && + isArrayMethodCallWithPredicate(context, services, parent) + ) { + const type = services.esTreeNodeToTSNodeMap.get(callback); + if (returnsThenable(checker, type)) { + context.report({ + messageId: 'predicate', + node: callback, + }); + } + } + } + } + function checkArguments( node: TSESTree.CallExpression | TSESTree.NewExpression, ): void { diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 83869ec9ad3c..e3e3a983d386 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -9,6 +9,7 @@ import { getParserServices, getTypeName, getTypeOfPropertyOfName, + isArrayMethodCallWithPredicate, isIdentifier, isNullableType, isTypeAnyType, @@ -458,26 +459,12 @@ export default createRule({ checkNode(node.test); } - const ARRAY_PREDICATE_FUNCTIONS = new Set([ - 'filter', - 'find', - 'some', - 'every', - ]); - function isArrayPredicateFunction(node: TSESTree.CallExpression): boolean { - const { callee } = node; - return ( - // looks like `something.filter` or `something.find` - callee.type === AST_NODE_TYPES.MemberExpression && - callee.property.type === AST_NODE_TYPES.Identifier && - ARRAY_PREDICATE_FUNCTIONS.has(callee.property.name) && - // and the left-hand side is an array, according to the types - (nodeIsArrayType(callee.object) || nodeIsTupleType(callee.object)) - ); - } function checkCallExpression(node: TSESTree.CallExpression): void { // If this is something like arr.filter(x => /*condition*/), check `condition` - if (isArrayPredicateFunction(node) && node.arguments.length) { + if ( + isArrayMethodCallWithPredicate(context, services, node) && + node.arguments.length + ) { const callback = node.arguments[0]; // Inline defined functions if ( diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 58f34597653b..b13b5855231d 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -21,6 +21,7 @@ export * from './scopeUtils'; export * from './types'; export * from './isAssignee'; export * from './getFixOrSuggest'; +export * from './isArrayMethodCallWithPredicate'; // this is done for convenience - saves migrating all of the old rules export * from '@typescript-eslint/type-utils'; diff --git a/packages/eslint-plugin/src/util/isArrayMethodCallWithPredicate.ts b/packages/eslint-plugin/src/util/isArrayMethodCallWithPredicate.ts new file mode 100644 index 000000000000..746e9003722c --- /dev/null +++ b/packages/eslint-plugin/src/util/isArrayMethodCallWithPredicate.ts @@ -0,0 +1,43 @@ +import { getConstrainedTypeAtLocation } from '@typescript-eslint/type-utils'; +import type { + ParserServicesWithTypeInformation, + TSESTree, +} from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; +import * as tsutils from 'ts-api-utils'; + +import { getStaticMemberAccessValue } from './misc'; + +const ARRAY_PREDICATE_FUNCTIONS = new Set([ + 'filter', + 'find', + 'findIndex', + 'findLast', + 'findLastIndex', + 'some', + 'every', +]); + +export function isArrayMethodCallWithPredicate( + context: RuleContext, + services: ParserServicesWithTypeInformation, + node: TSESTree.CallExpression, +): boolean { + if (node.callee.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + + const staticAccessValue = getStaticMemberAccessValue(node.callee, context); + + if (!staticAccessValue || !ARRAY_PREDICATE_FUNCTIONS.has(staticAccessValue)) { + return false; + } + + const checker = services.program.getTypeChecker(); + const type = getConstrainedTypeAtLocation(services, node.callee.object); + return tsutils + .unionTypeParts(type) + .flatMap(part => tsutils.intersectionTypeParts(part)) + .some(t => checker.isArrayType(t) || checker.isTupleType(t)); +} diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot index 17d39d27c875..8c9f4989d46b 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot @@ -14,6 +14,9 @@ if (promise) { const val = promise ? 123 : 456; ~~~~~~~ Expected non-Promise value in a boolean conditional. +[1, 2, 3].filter(() => promise); + ~~~~~~~~~~~~~ Expected a non-Promise value to be returned. + while (promise) { ~~~~~~~ Expected non-Promise value in a boolean conditional. // Do something @@ -34,6 +37,9 @@ if (await promise) { const val = (await promise) ? 123 : 456; +const returnVal = await promise; +[1, 2, 3].filter(() => returnVal); + while (await promise) { // Do something } diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 072e1444fea0..0c6b33582825 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -1047,6 +1047,10 @@ interface MyInterface extends MyCall, MyIndex, MyConstruct, MyMethods { 'const notAFn3: boolean = true;', 'const notAFn4: { prop: 1 } = { prop: 1 };', 'const notAFn5: {} = {};', + ` +const array: number[] = [1, 2, 3]; +array.filter(a => a > 1); + `, ], invalid: [ @@ -2269,5 +2273,54 @@ interface MyInterface extends MyCall, MyIndex, MyConstruct, MyMethods { }, ], }, + { + code: ` +declare function isTruthy(value: unknown): Promise; +[0, 1, 2].filter(isTruthy); + `, + errors: [ + { + line: 3, + messageId: 'predicate', + }, + ], + }, + { + code: ` +const array: number[] = []; +array.every(() => Promise.resolve(true)); + `, + errors: [ + { + line: 3, + messageId: 'predicate', + }, + ], + }, + { + code: ` +const array: (string[] & { foo: 'bar' }) | (number[] & { bar: 'foo' }) = []; +array.every(() => Promise.resolve(true)); + `, + errors: [ + { + line: 3, + messageId: 'predicate', + }, + ], + }, + { + code: ` +const tuple: [number, number, number] = [1, 2, 3]; +tuple.find(() => Promise.resolve(false)); + `, + options: [{ checksConditionals: true }], + errors: [ + { + line: 3, + messageId: 'predicate', + }, + ], + }, ], }); 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 b83fee7fd4f8..3ea760bc2402 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -1291,11 +1291,13 @@ function truthy() { function falsy() {} [1, 3, 5].filter(truthy); [1, 2, 3].find(falsy); +[1, 2, 3].findLastIndex(falsy); `, output: null, errors: [ ruleError(6, 18, 'alwaysTruthyFunc'), ruleError(7, 16, 'alwaysFalsyFunc'), + ruleError(8, 25, 'alwaysFalsyFunc'), ], }, // Supports generics