Skip to content

Commit fbe27ad

Browse files
fix(no-promise-reject): new Promises and throw statements are now also checked (#848)
1 parent 7217fa4 commit fbe27ad

File tree

4 files changed

+103
-9
lines changed

4 files changed

+103
-9
lines changed

docs/rules/no-promise-reject.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22

33
<!-- end auto-generated rule header -->
44

5-
This rule disallows use of `Promise.reject()`.
5+
This rule disallows rejecting promises.
66

77
## Rule Details
88

99
It is useful when using an `Option` type (something like `{ value: T } | { error: Error }`)
1010
for handling errors. In this case a promise should always resolve with an `Option` and never reject.
1111

12-
This rule should be used in conjunction with [`no-throw-statements`](./no-throw-statements.md).
13-
1412
### ❌ Incorrect
1513

1614
<!-- eslint-skip -->

src/rules/no-promise-reject.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import {
88
type NamedCreateRuleCustomMeta,
99
type RuleResult,
1010
} from "#/utils/rule";
11-
import { isIdentifier, isMemberExpression } from "#/utils/type-guards";
11+
import { getEnclosingFunction, getEnclosingTryStatement } from "#/utils/tree";
12+
import {
13+
isFunctionLike,
14+
isIdentifier,
15+
isMemberExpression,
16+
} from "#/utils/type-guards";
1217

1318
/**
1419
* The name of this rule.
@@ -39,7 +44,7 @@ const defaultOptions: Options = [{}];
3944
* The possible error messages.
4045
*/
4146
const errorMessages = {
42-
generic: "Unexpected reject, return an error instead.",
47+
generic: "Unexpected rejection, resolve an error instead.",
4348
} as const;
4449

4550
/**
@@ -67,6 +72,7 @@ function checkCallExpression(
6772
return {
6873
context,
6974
descriptors:
75+
// TODO: Better Promise type detection.
7076
isMemberExpression(node.callee) &&
7177
isIdentifier(node.callee.object) &&
7278
isIdentifier(node.callee.property) &&
@@ -77,12 +83,65 @@ function checkCallExpression(
7783
};
7884
}
7985

86+
/**
87+
* Check if the given NewExpression is for a Promise and it has a callback that rejects.
88+
*/
89+
function checkNewExpression(
90+
node: TSESTree.NewExpression,
91+
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
92+
): RuleResult<keyof typeof errorMessages, Options> {
93+
return {
94+
context,
95+
descriptors:
96+
// TODO: Better Promise type detection.
97+
isIdentifier(node.callee) &&
98+
node.callee.name === "Promise" &&
99+
node.arguments[0] !== undefined &&
100+
isFunctionLike(node.arguments[0]) &&
101+
node.arguments[0].params.length === 2
102+
? [{ node: node.arguments[0].params[1]!, messageId: "generic" }]
103+
: [],
104+
};
105+
}
106+
107+
/**
108+
* Check if the given ThrowStatement violates this rule.
109+
*/
110+
function checkThrowStatement(
111+
node: TSESTree.ThrowStatement,
112+
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
113+
): RuleResult<keyof typeof errorMessages, Options> {
114+
const enclosingFunction = getEnclosingFunction(node);
115+
if (enclosingFunction?.async !== true) {
116+
return { context, descriptors: [] };
117+
}
118+
119+
const enclosingTryStatement = getEnclosingTryStatement(node);
120+
if (
121+
enclosingTryStatement === null ||
122+
getEnclosingFunction(enclosingTryStatement) !== enclosingFunction ||
123+
enclosingTryStatement.handler === null
124+
) {
125+
return {
126+
context,
127+
descriptors: [{ node, messageId: "generic" }],
128+
};
129+
}
130+
131+
return {
132+
context,
133+
descriptors: [],
134+
};
135+
}
136+
80137
// Create the rule.
81138
export const rule = createRule<keyof typeof errorMessages, Options>(
82139
name,
83140
meta,
84141
defaultOptions,
85142
{
86143
CallExpression: checkCallExpression,
144+
NewExpression: checkNewExpression,
145+
ThrowStatement: checkThrowStatement,
87146
},
88147
);

src/utils/tree.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
isTSTypeAnnotation,
2525
isTSTypeLiteral,
2626
isTSTypeReference,
27+
isTryStatement,
2728
isVariableDeclaration,
2829
} from "./type-guards";
2930

@@ -52,7 +53,21 @@ export function isInFunctionBody(
5253
node: TSESTree.Node,
5354
async?: boolean,
5455
): boolean {
55-
const functionNode = getAncestorOfType(
56+
const functionNode = getEnclosingFunction(node);
57+
58+
return (
59+
functionNode !== null &&
60+
(async === undefined || functionNode.async === async)
61+
);
62+
}
63+
64+
/**
65+
* Get the function the given node is in.
66+
*
67+
* Will return null if not in a function.
68+
*/
69+
export function getEnclosingFunction(node: TSESTree.Node) {
70+
return getAncestorOfType(
5671
(
5772
n,
5873
c,
@@ -62,10 +77,17 @@ export function isInFunctionBody(
6277
| TSESTree.FunctionExpression => isFunctionLike(n) && n.body === c,
6378
node,
6479
);
80+
}
6581

66-
return (
67-
functionNode !== null &&
68-
(async === undefined || functionNode.async === async)
82+
/**
83+
* Get the function the given node is in.
84+
*
85+
* Will return null if not in a function.
86+
*/
87+
export function getEnclosingTryStatement(node: TSESTree.Node) {
88+
return getAncestorOfType(
89+
(n, c): n is TSESTree.TryStatement => isTryStatement(n) && n.block === c,
90+
node,
6991
);
7092
}
7193

src/utils/type-guards.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,12 @@ export function isThrowStatement(
237237
return node.type === AST_NODE_TYPES.ThrowStatement;
238238
}
239239

240+
export function isTryStatement(
241+
node: TSESTree.Node,
242+
): node is TSESTree.TryStatement {
243+
return node.type === AST_NODE_TYPES.TryStatement;
244+
}
245+
240246
export function isTSArrayType(
241247
node: TSESTree.Node,
242248
): node is TSESTree.TSArrayType {
@@ -440,3 +446,12 @@ export function isObjectConstructorType(type: Type | null): boolean {
440446
export function isFunctionLikeType(type: Type | null): boolean {
441447
return type !== null && type.getCallSignatures().length > 0;
442448
}
449+
450+
export function isPromiseType(type: Type | null): boolean {
451+
return (
452+
type !== null &&
453+
(((type.symbol as unknown) !== undefined &&
454+
type.symbol.name === "Promise") ||
455+
(isUnionType(type) && type.types.some(isPromiseType)))
456+
);
457+
}

0 commit comments

Comments
 (0)