8000 feat(eslint-plugin): [no-misused-promises] check array predicate retu… · ronami/typescript-eslint@454d37e · GitHub
[go: up one dir, main page]

Skip to content

Commit 454d37e

Browse files
authored
feat(eslint-plugin): [no-misused-promises] check array predicate return (typescript-eslint#9955)
* feat(eslint-plugin): [no-misused-promises] check array predicate return * refactor * fix review
1 parent 95a947a commit 454d37e

File tree

8 files changed

+138
-18
lines changed

8 files changed

+138
-18
lines changed

packages/eslint-plugin/docs/rules/no-misused-promises.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ if (promise) {
134134

135135
const val = promise ? 123 : 456;
136136

137+
[1, 2, 3].filter(() => promise);
138+
137139
while (promise) {
138140
// Do something
139141
}
@@ -152,6 +154,9 @@ if (await promise) {
152154

153155
const val = (await promise) ? 123 : 456;
154156

157+
const returnVal = await promise;
158+
[1, 2, 3].filter(() => returnVal);
159+
155160
while (await promise) {
156161
// Do something
157162
}

packages/eslint-plugin/src/rules/no-misused-promises.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as ts from 'typescript';
66
import {
77
createRule,
88
getParserServices,
9+
isArrayMethodCallWithPredicate,
910
isFunction,
1011
isRestParameterDeclaration,
1112
nullThrows,
@@ -31,6 +32,7 @@ interface ChecksVoidReturnOptions {
3132

3233
type MessageId =
3334
| 'conditional'
35+
| 'predicate'
3436
| 'spread'
3537
| 'voidReturnArgument'
3638
| 'voidReturnAttribute'
@@ -91,6 +93,7 @@ export default createRule<Options, MessageId>({
9193
voidReturnVariable:
9294
'Promise-returning function provided to variable where a void return was expected.',
9395
conditional: 'Expected non-Promise value in a boolean conditional.',
96+
predicate: 'Expected a non-Promise value to be returned.',
9497
spread: 'Expected a non-Promise value to be spreaded in an object.',
9598
},
9699
schema: [
@@ -175,6 +178,7 @@ export default createRule<Options, MessageId>({
175178
checkConditional(node.argument, true);
176179
},
177180
WhileStatement: checkTestConditional,
181+
'CallExpression > MemberExpression': checkArrayPredicates,
178182
};
179183

180184
checksVoidReturn = parseChecksVoidReturn(checksVoidReturn);
@@ -322,6 +326,25 @@ export default createRule<Options, MessageId>({
322326
}
323327
}
324328

329+
function checkArrayPredicates(node: TSESTree.MemberExpression): void {
330+
const parent = node.parent;
331+
if (parent.type === AST_NODE_TYPES.CallExpression) {
332+
const callback = parent.arguments.at(0);
333+
if (
334+
callback &&
335+
isArrayMethodCallWithPredicate(context, services, parent)
336+
) {
337+
const type = services.esTreeNodeToTSNodeMap.get(callback);
338+
if (returnsThenable(checker, type)) {
339+
context.report({
340+
messageId: 'predicate',
341+
node: callback,
342+
});
343+
}
344+
}
345+
}
346+
}
347+
325348
function checkArguments(
326349
node: TSESTree.CallExpression | TSESTree.NewExpression,
327350
): void {

packages/eslint-plugin/src/rules/no-unnecessary-condition.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getParserServices,
1010
getTypeName,
1111
getTypeOfPropertyOfName,
12+
isArrayMethodCallWithPredicate,
1213
isIdentifier,
1314
isNullableType,
1415
isTypeAnyType,
@@ -461,26 +462,12 @@ export default createRule<Options, MessageId>({
461462
checkNode(node.test);
462463
}
463464

464-
const ARRAY_PREDICATE_FUNCTIONS = new Set([
465-
'filter',
466-
'find',
467-
'some',
468-
'every',
469-
]);
470-
function isArrayPredicateFunction(node: TSESTree.CallExpression): boolean {
471-
const { callee } = node;
472-
return (
473-
// looks like `something.filter` or `something.find`
474-
callee.type === AST_NODE_TYPES.MemberExpression &&
475-
callee.property.type === AST_NODE_TYPES.Identifier &&
476-
ARRAY_PREDICATE_FUNCTIONS.has(callee.property.name) &&
477-
// and the left-hand side is an array, according to the types
478-
(nodeIsArrayType(callee.object) || nodeIsTupleType(callee.object))
479-
);
480-
}
481465
function checkCallExpression(node: TSESTree.CallExpression): void {
482466
// If this is something like arr.filter(x => /*condition*/), check `condition`
483-
if (isArrayPredicateFunction(node) && node.arguments.length) {
467+
if (
468+
isArrayMethodCallWithPredicate(context, services, node) &&
469+
node.arguments.length
470+
) {
484471
const callback = node.arguments[0];
485472
// Inline defined functions
486473
if (

packages/eslint-plugin/src/util/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './scopeUtils';
2121
export * from './types';
2222
export * from './isAssignee';
2323
export * from './getFixOrSuggest';
24+
export * from './isArrayMethodCallWithPredicate';
2425

2526
// this is done for convenience - saves migrating all of the old rules
2627
export * from '@typescript-eslint/type-utils';
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { getConstrainedTypeAtLocation } from '@typescript-eslint/type-utils';
2+
import type {
3+
ParserServicesWithTypeInformation,
4+
TSESTree,
5+
} from '@typescript-eslint/utils';
6+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
7+
import type { RuleContext } from '@typescript-eslint/utils/ts-eslint';
8+
import * as tsutils from 'ts-api-utils';
9+
10+
import { getStaticMemberAccessValue } from './misc';
11+
12+
const ARRAY_PREDICATE_FUNCTIONS = new Set([
13+
'filter',
14+
'find',
15+
'findIndex',
16+
'findLast',
17+
'findLastIndex',
18+
'some',
19+
'every',
20+
]);
21+
22+
export function isArrayMethodCallWithPredicate(
23+
context: RuleContext<string, unknown[]>,
24+
services: ParserServicesWithTypeInformation,
25+
node: TSESTree.CallExpression,
26+
): boolean {
27+
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
28+
return false;
29+
}
30+
31+
const staticAccessValue = getStaticMemberAccessValue(node.callee, context);
32+
33+
if (!staticAccessValue || !ARRAY_PREDICATE_FUNCTIONS.has(staticAccessValue)) {
34+
return false;
35+
}
36+
37+
const checker = services.program.getTypeChecker();
38+
const type = getConstrainedTypeAtLocation(services, node.callee.object);
39+
return tsutils
40+
.unionTypeParts(type)
41+
.flatMap(part => tsutils.intersectionTypeParts(part))
42+
.some(t => checker.isArrayType(t) || checker.isTupleType(t));
43+
}

packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-misused-promises.shot

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/eslint-plugin/tests/rules/no-misused-promises.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,10 @@ interface MyInterface extends MyCall, MyIndex, MyConstruct, MyMethods {
10471047
'const notAFn3: boolean = true;',
10481048
'const notAFn4: { prop: 1 } = { prop: 1 };',
10491049
'const notAFn5: {} = {};',
1050+
`
1051+
const array: number[] = [1, 2, 3];
1052+
array.filter(a => a > 1);
1053+
`,
10501054
],
10511055

10521056
invalid: [
@@ -2269,5 +2273,54 @@ interface MyInterface extends MyCall, MyIndex, MyConstruct, MyMethods {
22692273
},
22702274
],
22712275
},
2276+
{
2277+
code: `
2278+
declare function isTruthy(value: unknown): Promise<boolean>;
2279+
[0, 1, 2].filter(isTruthy);
2280+
`,
2281+
errors: [
2282+
{
2283+
line: 3,
2284+
messageId: 'predicate',
2285+
},
2286+
],
2287+
},
2288+
{
2289+
code: `
2290+
const array: number[] = [];
2291+
array.every(() => Promise.resolve(true));
2292+
`,
2293+
errors: [
2294+
{
2295+
line: 3,
2296+
messageId: 'predicate',
2297+
},
2298+
],
2299+
},
2300+
{
2301+
code: `
2302+
const array: (string[] & { foo: 'bar' }) | (number[] & { bar: 'foo' }) = [];
2303+
array.every(() => Promise.resolve(true));
2304+
`,
2305+
errors: [
2306+
{
2307+
line: 3,
2308+
messageId: 'predicate',
2309+
},
2310+
],
2311+
},
2312+
{
2313+
code: `
2314+
const tuple: [number, number, number] = [1, 2, 3];
2315+
tuple.find(() => Promise.resolve(false));
2316+
`,
2317+
options: [{ checksConditionals: true }],
2318+
errors: [
2319+
{
2320+
line: 3,
2321+
messageId: 'predicate',
2322+
},
2323+
],
2324+
},
22722325
],
22732326
});

packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,11 +1300,13 @@ function truthy() {
13001300
function falsy() {}
13011301
[1, 3, 5].filter(truthy);
13021302
[1, 2, 3].find(falsy);
1303+
[1, 2, 3].findLastIndex(falsy);
13031304
`,
13041305
output: null,
13051306
errors: [
13061307
ruleError(6, 18, 'alwaysTruthyFunc'),
13071308
ruleError(7, 16, 'alwaysFalsyFunc'),
1309+
ruleError(8, 25, 'alwaysFalsyFunc'),
13081310
],
13091311
},
13101312
// Supports generics

0 commit comments

Comments
 (0)
0