-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(eslint-plugin): [prefer-promise-reject-errors] add rule #8011
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
JoshuaKGoldberg
merged 36 commits into
typescript-eslint:main
from
auvred:feat/prefer-promise-reject-errors-rule
Jan 9, 2024
Merged
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
a4f7195
feat(eslint-plugin): [prefer-promise-reject-errors] new rule!
auvred 538323f
test: ~100% coverage
auvred f779d93
docs: add rule docs
auvred a27930a
test: add some cases
auvred 2b83ea4
chore: lint --fix
auvred d86f5c0
chore: reformat tests
auvred dab6503
feat: add support for literal computed reject name
auvred 8d8bbc3
chore: lint --fix
auvred ff307be
refactor: get rid of one @ts-expect-error
auvred ccde4a6
docs: refer to the original rule description
auvred d153d13
test: add few cases
auvred 5263fa8
docs: remove some examples
auvred 6981eb5
refactor: move check if symbol is from default lib or not to new fn
auvred a680b8d
refactor: assert that rejectVariable is non-nullable
auvred 1e2e771
chore: remove assertion in skipChainExpression
auvred e1db988
test: specify error ranges for invalid test cases
auvred 1401910
chore: merge main
auvred 42e5c1f
chore: format tests
auvred 47eb018
chore: remove unused check if variable reference is read or not
auvred f2ee5d9
chore: include rule to `strict-type-checked` config
auvred 0229ae7
refactor: simplify isSymbolFromDefaultLibrary
auvred 9db6d84
chore: remove ts-expect-error comment
auvred a0699cf
feat: add checks for Promise child classes and unions/intersections
auvred 3a79f84
Merge branch 'main' into feat/prefer-promise-reject-errors-rule
auvred 1c97219
Update packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md
auvred 42c5737
refactor: `program` -> `services.program`
auvred 184de0f
refactor: split unreadable if condition
auvred eaab9eb
docs: simplify examples
auvred 895f1b5
refactor: rename `isBuiltinSymbolLike.ts` -> `builtinSymbolLikes.ts`
auvred 584c2e7
Merge branch 'main' into feat/prefer-promise-reject-errors-rule
auvred f186084
perf: get type of `reject` callee lazily
auvred e4af6ed
test: add cases with arrays,never,unknown
auvred 2326429
feat: add support for `Readonly<Error>` and similar
auvred f33a84a
chore: fix lint issues
auvred 7236787
Merge branch 'main' into feat/prefer-promise-reject-errors-rule
auvred bd20782
Merge branch 'main' into feat/prefer-promise-reject-errors-rule
auvred File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
50 changes: 50 additions & 0 deletions
50
packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
--- | ||
description: 'Require using Error objects as Promise rejection reasons.' | ||
--- | ||
|
||
> 🛑 This file is source code, not the primary documentation location! 🛑 | ||
> | ||
> See **https://typescript-eslint.io/rules/prefer-promise-reject-errors** for documentation. | ||
|
||
This rule extends the base [`eslint/prefer-promise-reject-errors`](https://eslint.org/docs/rules/prefer-promise-reject-errors) rule. | ||
It uses type information to enforce that `Promise`s are only rejected with `Error` objects. | ||
|
||
## Examples | ||
|
||
<!--tabs--> | ||
|
||
### ❌ Incorrect | ||
|
||
```ts | ||
Promise.reject('error'); | ||
|
||
const err = new Error(); | ||
Promise.reject('an ' + err); | ||
|
||
new Promise((resolve, reject) => reject('error')); | ||
|
||
new Promise((resolve, reject) => { | ||
const err = new Error(); | ||
reject('an ' + err); | ||
}); | ||
``` | ||
|
||
### ✅ Correct | ||
|
||
```ts | ||
Promise.reject(new Error()); | ||
|
||
class CustomError extends Error { | ||
// ... | ||
} | ||
Promise.reject(new CustomError()); | ||
|
||
new Promise((resolve, reject) => reject(new Error())); | ||
|
||
new Promise((resolve, reject) => { | ||
class CustomError extends Error { | ||
// ... | ||
} | ||
return reject(new CustomError()); | ||
}); | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import type { TSESTree } from '@typescript-eslint/utils'; | ||
import { AST_NODE_TYPES } from '@typescript-eslint/utils'; | ||
import { getDeclaredVariables } from '@typescript-eslint/utils/eslint-utils'; | ||
|
||
import { | ||
createRule, | ||
getParserServices, | ||
isErrorLike, | ||
isFunction, | ||
isIdentifier, | ||
isPromiseConstructorLike, | ||
isPromiseLike, | ||
isReadonlyErrorLike, | ||
} from '../util'; | ||
|
||
export type MessageIds = 'rejectAnError'; | ||
|
||
export type Options = [ | ||
{ | ||
allowEmptyReject?: boolean; | ||
}, | ||
]; | ||
|
||
export default createRule<Options, MessageIds>({ | ||
name: 'prefer-promise-reject-errors', | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
auvred marked this conversation as resolved.
Show resolved
Hide resolved
|
||
description: 'Require using Error objects as Promise rejection reasons', | ||
recommended: 'strict', | ||
extendsBaseRule: true, | ||
requiresTypeChecking: true, | ||
}, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
allowEmptyReject: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
messages: { | ||
rejectAnError: 'Expected the Promise rejection reason to be an Error.', | ||
}, | ||
}, | ||
defaultOptions: [ | ||
{ | ||
allowEmptyReject: false, | ||
}, | ||
], | ||
create(context, [options]) { | ||
const services = getParserServices(context); | ||
|
||
function checkRejectCall(callExpression: TSESTree.CallExpression): void { | ||
const argument = callExpression.arguments.at(0); | ||
if (argument) { | ||
const type = services.getTypeAtLocation(argument); | ||
if ( | ||
isErrorLike(services.program, type) || | ||
isReadonlyErrorLike(services.program, type) | ||
) { | ||
return; | ||
} | ||
} else if (options.allowEmptyReject) { | ||
return; | ||
} | ||
|
||
context.report({ | ||
node: callExpression, | ||
messageId: 'rejectAnError', | ||
}); | ||
} | ||
|
||
function skipChainExpression<T extends TSESTree.Node>( | ||
node: T, | ||
): T | TSESTree.ChainElement { | ||
return node.type === AST_NODE_TYPES.ChainExpression | ||
? node.expression | ||
: node; | ||
} | ||
|
||
function typeAtLocationIsLikePromise(node: TSESTree.Node): boolean { | ||
const type = services.getTypeAtLocation(node); | ||
return ( | ||
isPromiseConstructorLike(services.program, type) || | ||
isPromiseLike(services.program, type) | ||
); | ||
} | ||
|
||
return { | ||
CallExpression(node): void { | ||
const callee = skipChainExpression(node.callee); | ||
|
||
if (callee.type !== AST_NODE_TYPES.MemberExpression) { | ||
return; | ||
} | ||
|
||
const rejectMethodCalled = callee.computed | ||
? callee.property.type === AST_NODE_TYPES.Literal && | ||
callee.property.value === 'reject' | ||
: callee.property.name === 'reject'; | ||
|
||
if ( | ||
!rejectMethodCalled || | ||
!typeAtLocationIsLikePromise(callee.object) | ||
) { | ||
auvred marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return; | ||
} | ||
|
||
checkRejectCall(node); | ||
}, | ||
NewExpression(node): void { | ||
const callee = skipChainExpression(node.callee); | ||
if ( | ||
!isPromiseConstructorLike( | ||
services.program, | ||
services.getTypeAtLocation(callee), | ||
) | ||
) { | ||
return; | ||
} | ||
|
||
const executor = node.arguments.at(0); | ||
if (!executor || !isFunction(executor)) { | ||
return; | ||
} | ||
const rejectParamNode = executor.params.at(1); | ||
if (!rejectParamNode || !isIdentifier(rejectParamNode)) { | ||
return; | ||
} | ||
|
||
// reject param is always present in variables declared by executor | ||
const rejectVariable = getDeclaredVariables(context, executor).find( | ||
variable => variable.identifiers.includes(rejectParamNode), | ||
)!; | ||
|
||
rejectVariable.references.forEach(ref => { | ||
if ( | ||
ref.identifier.parent.type !== AST_NODE_TYPES.CallExpression || | ||
ref.identifier !== ref.identifier.parent.callee | ||
) { | ||
return; | ||
} | ||
|
||
checkRejectCall(ref.identifier.parent); | ||
}); | ||
}, | ||
}; | ||
}, | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.