Skip to content

Commit c20bd2f

Browse files
feat(eslint-plugin): [no-unsafe-return] check promise any (#8693)
* feat(eslint-plugin): [no-unsafe-return] check promise any * add testcases * apply reviews * fix lint errors * refactor * add type * improve messages * update comment * apply review * fix * fix tests * update docs * apply review * fix tests * change to use internal getAwaitedType * add todo comment * Update packages/eslint-plugin/docs/rules/no-unsafe-return.mdx Co-authored-by: Josh Goldberg ✨ <[email protected]> * apply reviews --------- Co-authored-by: Josh Goldberg ✨ <[email protected]>
1 parent 3d9ae44 commit c20bd2f

File tree

7 files changed

+375
-26
lines changed

7 files changed

+375
-26
lines changed

packages/eslint-plugin/docs/rules/no-unsafe-return.mdx

+9-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Using `any` disables many type checking rules and is generally best used only as
1515
Despite your best intentions, the `any` type can sometimes leak into your codebase.
1616
Returning an an `any`-typed value from a function creates a potential type safety hole and source of bugs in your codebase.
1717

18-
This rule disallows returning `any` or `any[]` from a function.
18+
This rule disallows returning `any` or `any[]` from a function and returning `Promise<any>` from an async function.
1919

2020
This rule also compares generic type argument types to ensure you don't return an unsafe `any` in a generic position to a function that's expecting a specific type.
2121
For example, it will error if you return `Set<any>` from a function declared as returning `Set<string>`.
@@ -56,6 +56,10 @@ const foo10 = () => [] as any[];
5656

5757
const foo11 = (): string[] => [1, 2, 3] as any[];
5858

59+
async function foo13() {
60+
return Promise.resolve({} as any);
61+
}
62+
5963
// generic position examples
6064
function assignability1(): Set<string> {
6165
return new Set<any>([1]);
@@ -78,6 +82,10 @@ function foo2() {
7882
const foo3 = () => [];
7983
const foo4 = () => ['a'];
8084

85+
async function foo5() {
86+
return Promise.resolve(1);
87+
}
88+
8189
function assignability1(): Set<string> {
8290
return new Set<string>(['foo']);
8391
}

packages/eslint-plugin/src/rules/no-unsafe-return.ts

+50-10
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import * as ts from 'typescript';
66
import {
77
AnyType,
88
createRule,
9+
discriminateAnyType,
910
getConstrainedTypeAtLocation,
1011
getContextualType,
1112
getParserServices,
1213
getThisExpression,
13-
isAnyOrAnyArrayTypeDiscriminated,
1414
isTypeAnyType,
1515
isTypeFlagSet,
1616
isTypeUnknownArrayType,
@@ -28,9 +28,9 @@ export default createRule({
2828
requiresTypeChecking: true,
2929
},
3030
messages: {
31-
unsafeReturn: 'Unsafe return of an {{type}} typed value.',
31+
unsafeReturn: 'Unsafe return of a value of type {{type}}.',
3232
unsafeReturnThis: [
33-
'Unsafe return of an `{{type}}` typed value. `this` is typed as `any`.',
33+
'Unsafe return of a value of type `{{type}}`. `this` is typed as `any`.',
3434
'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.',
3535
].join('\n'),
3636
unsafeReturnAssignment:
@@ -78,7 +78,14 @@ export default createRule({
7878
reportingNode: TSESTree.Node = returnNode,
7979
): void {
8080
const tsNode = services.esTreeNodeToTSNodeMap.get(returnNode);
81-
const anyType = isAnyOrAnyArrayTypeDiscriminated(tsNode, checker);
81+
const type = checker.getTypeAtLocation(tsNode);
82+
83+
const anyType = discriminateAnyType(
84+
type,
85+
checker,
86+
services.program,
87+
tsNode,
88+
);
8289
const functionNode = getParentFunctionNode(returnNode);
8390
/* istanbul ignore if */ if (!functionNode) {
8491
return;
@@ -100,27 +107,46 @@ export default createRule({
100107
if (!functionType) {
101108
functionType = services.getTypeAtLocation(functionNode);
102109
}
103-
110+
const callSignatures = tsutils.getCallSignaturesOfType(functionType);
104111
// If there is an explicit type annotation *and* that type matches the actual
105112
// function return type, we shouldn't complain (it's intentional, even if unsafe)
106113
if (functionTSNode.type) {
107-
for (const signature of tsutils.getCallSignaturesOfType(functionType)) {
114+
for (const signature of callSignatures) {
115+
const signatureReturnType = signature.getReturnType();
116+
108117
if (
109-
returnNodeType === signature.getReturnType() ||
118+
returnNodeType === signatureReturnType ||
110119
isTypeFlagSet(
111-
signature.getReturnType(),
120+
signatureReturnType,
112121
ts.TypeFlags.Any | ts.TypeFlags.Unknown,
113122
)
114123
) {
115124
return;
116125
}
126+
if (functionNode.async) {
127+
const awaitedSignatureReturnType =
128+
checker.getAwaitedType(signatureReturnType);
129+
130+
const awaitedReturnNodeType =
131+
checker.getAwaitedType(returnNodeType);
132+
if (
133+
awaitedReturnNodeType === awaitedSignatureReturnType ||
134+
(awaitedSignatureReturnType &&
135+
isTypeFlagSet(
136+
awaitedSignatureReturnType,
137+
ts.TypeFlags.Any | ts.TypeFlags.Unknown,
138+
))
139+
) {
140+
return;
141+
}
142+
}
117143
}
118144
}
119145

120146
if (anyType !== AnyType.Safe) {
121147
// Allow cases when the declared return type of the function is either unknown or unknown[]
122148
// and the function is returning any or any[].
123-
for (const signature of functionType.getCallSignatures()) {
149+
for (const signature of callSignatures) {
124150
const functionReturnType = signature.getReturnType();
125151
if (
126152
anyType === AnyType.Any &&
@@ -134,6 +160,18 @@ export default createRule({
134160
) {
135161
return;
136162
}
163+
const awaitedType = checker.getAwaitedType(functionReturnType);
164+
if (
165+
awaitedType &&
166+
anyType === AnyType.PromiseAny &&
167+
isTypeUnknownType(awaitedType)
168+
) {
169+
return;
170+
}
171+
}
172+
173+
if (anyType === AnyType.PromiseAny && !functionNode.async) {
174+
return;
137175
}
138176

139177
let messageId: 'unsafeReturn' | 'unsafeReturnThis' = 'unsafeReturn';
@@ -161,7 +199,9 @@ export default createRule({
161199
? 'error'
162200
: anyType === AnyType.Any
163201
? '`any`'
164-
: '`any[]`',
202+
: anyType === AnyType.PromiseAny
203+
? '`Promise<any>`'
204+
: '`any[]`',
165205
},
166206
});
167207
}

packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-return.shot

+20-11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)