This commit is contained in:
Lilith 2024-06-13 00:09:21 +02:00
parent eddf7cecb8
commit aea798d119
Signed by: lilith
GPG key ID: 8712A0F317C37175
16631 changed files with 1480363 additions and 257 deletions

View file

@ -0,0 +1,190 @@
## 6.0.2
- Added tests for @typescript-eslint/parser support
## 6.0.1
- Fixed @typescript-eslint/parser issue #331, #205
## 6.0.0
- Dropped node 10 from engines #231
- Updated a ton of deps #236, #237, #235, #234
- ESLint 8 support #219
## 5.2.0
- Updated `param-names` rule to allow for unused params
## 5.1.1
- Updated docs to include `no-callback-in-promise` reasons #215
## 5.1.0
- Included `catch()` and `finally()` in `prefer-await-to-then` #196
- Added some additional tests and upgraded some dev deps #196
- Exempted array methods in prefer-await-to-callbacks
([#212](https://github.com/eslint-community/eslint-plugin-promise/issues/212))
## 5.0.0
- ESLint 7.0 Support
## 4.3.1.
- Updated and applied prettier
## 4.3.0
- https://github.com/eslint-community/eslint-plugin-promise/pull/202
- Updated jest
## 4.2.2
- Added license
- Dependabot security updates
## 4.2.1
- Added more use cases to `no-return-wrap`
## 4.0.1
- Remove `promise/param-names` fixer
([#146](https://github.com/eslint-community/eslint-plugin-promise/pull/146))
## 4.0.0
- Added fixer for `promise/no-new-statics` rule
([#133](https://github.com/eslint-community/eslint-plugin-promise/pull/133))
- Support ESLint v5
([#144](https://github.com/eslint-community/eslint-plugin-promise/pull/144))
This is a breaking change that drops support for Node v4. In order to use ESLint
v5 and eslint-plugin-promise v4, you must use Node >=6.
## 3.8.0
- Removed `promise/avoid-new` from recommended configuration
([#119](https://github.com/eslint-community/eslint-plugin-promise/pull/119))
- Ignored event listener callbacks in `promise/prefer-await-to-callbacks`
([#117](https://github.com/eslint-community/eslint-plugin-promise/pull/117))
- Ignored top-level awaits in `promise/prefer-await-to-then`
([#126](https://github.com/eslint-community/eslint-plugin-promise/pull/126))
- Added docs for `promise/no-nesting` and `promise/prefer-await-to-then`
([#120](https://github.com/eslint-community/eslint-plugin-promise/pull/120))
([#121](https://github.com/eslint-community/eslint-plugin-promise/pull/121))
## 3.7.0
- Added `promise/valid-params` rule
([#85](https://github.com/eslint-community/eslint-plugin-promise/pull/85))
- Added `promise/no-new-statics` rule
([#82](https://github.com/eslint-community/eslint-plugin-promise/pull/82))
- Added fixer for `promise/param-names` rule
([#99](https://github.com/eslint-community/eslint-plugin-promise/pull/99))
- Added rule documentation to each rule
([#91](https://github.com/eslint-community/eslint-plugin-promise/pull/91))
## 3.6.0
- Added `['catch']` support in `catch-or-return`
- Added `no-return-in-finally` rule
- Fixed some formatting in the docs
- Added `allowReject` option to `no-return-wrap`
- Added exceptions for `no-callback-in-promise`
## 3.5.0
- Added support for recommended settings using
`extends: plugin:promise/recommended`
## 3.4.2
- Fixed always return false positive with ternary (#31)
## 3.4.1
- fixed #49
## 3.4.0
- new rule: avoid-new
- new rule: no-promise-in-callback
- new rule: no-callback-in-promise
- new rule: no-nesting
## 3.3.2
- Removed eslint from peerDeps
## 3.3.1
- Updated engines with proper stuff
- Fixed bug for unreachable code
## 3.3.0
- Rule: `prefer-async-to-callbacks` added
- Rule: `prefer-async-to-then` added
## 3.2.1
- Fix: `no-return-wrap` rule missing from index.js
## 3.2.0
- Added `no-return-wrap` rule
## 3.1.0
- Added multiple terminationMethods
## 3.0.1
- Removed deprecated `always-catch` rule
- FIX: always-return error with "fn && fn()"
## 3.0.0
- Updated column and line numbers
- Added flow analysis for better handling of if statements
## 2.0.1
- Fixed type in docs
## 2.0.0
- ESLint 3.0 Support
## 1.3.2
- Updated tests to run on eslint 2.0
- Fixed some issues with `no-native` rule
## 1.3.1
- Actually added `no-native` rule
## 1.3.0
- Added `no-native` rule
## 1.2.0
- Allow `throw` in `always-return` rule
- Added `terminationMethod` option to `catch-or-return` rule
## 1.1.0
- Added `catch-or-return` rule
## 1.0.8
- Fixed crash issues
## 1.0.0 - 1.0.7
- Lots of basic feature updates and doc changes

13
home/ags/node_modules/eslint-plugin-promise/LICENSE.md generated vendored Normal file
View file

@ -0,0 +1,13 @@
Copyright (c) 2020, Jamund Ferguson
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.

123
home/ags/node_modules/eslint-plugin-promise/README.md generated vendored Normal file
View file

@ -0,0 +1,123 @@
# eslint-plugin-promise
Enforce best practices for JavaScript promises.
[![CI](https://github.com/eslint-community/eslint-plugin-promise/actions/workflows/CI.yml/badge.svg)](https://github.com/eslint-community/eslint-plugin-promise/actions/workflows/CI.yml)
[![npm version](https://badge.fury.io/js/eslint-plugin-promise.svg)](https://www.npmjs.com/package/eslint-plugin-promise)
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Installation](#installation)
- [Usage](#usage)
- [Rules](#rules)
- [Maintainers](#maintainers)
- [License](#license)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Installation
You'll first need to install [ESLint](http://eslint.org):
```sh
npm install eslint --save-dev
```
Next, install `eslint-plugin-promise`:
```sh
npm install eslint-plugin-promise --save-dev
```
**Note:** If you installed ESLint globally (using the `-g` flag) then you must
also install `eslint-plugin-promise` globally.
## Usage
Add `promise` to the plugins section of your `.eslintrc.json` configuration
file. You can omit the `eslint-plugin-` prefix:
```json
{
"plugins": ["promise"]
}
```
Then configure the rules you want to use under the rules section.
```json
{
"rules": {
"promise/always-return": "error",
"promise/no-return-wrap": "error",
"promise/param-names": "error",
"promise/catch-or-return": "error",
"promise/no-native": "off",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn",
"promise/avoid-new": "warn",
"promise/no-new-statics": "error",
"promise/no-return-in-finally": "warn",
"promise/valid-params": "warn"
}
}
```
or start with the recommended rule set:
```json
{
"extends": ["plugin:promise/recommended"]
}
```
## Rules
<!-- begin auto-generated rules list -->
💼 Configurations enabled in.\
⚠️ Configurations set to warn in.\
🚫 Configurations disabled in.\
✅ Set in the `recommended` configuration.\
🔧 Automatically fixable by the
[`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
| Name                      | Description | 💼 | ⚠️ | 🚫 | 🔧 |
| :------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | :-- | :-- | :-- | :-- |
| [always-return](docs/rules/always-return.md) | Require returning inside each `then()` to create readable and reusable Promise chains. | ✅ | | | |
| [avoid-new](docs/rules/avoid-new.md) | Disallow creating `new` promises outside of utility libs (use [pify][] instead). | | | ✅ | |
| [catch-or-return](docs/rules/catch-or-return.md) | Enforce the use of `catch()` on un-returned promises. | ✅ | | | |
| [no-callback-in-promise](docs/rules/no-callback-in-promise.md) | Disallow calling `cb()` inside of a `then()` (use [nodeify][] instead). | | ✅ | | |
| [no-multiple-resolved](docs/rules/no-multiple-resolved.md) | Disallow creating new promises with paths that resolve multiple times. | | | | |
| [no-native](docs/rules/no-native.md) | Require creating a `Promise` constructor before using it in an ES5 environment. | | | ✅ | |
| [no-nesting](docs/rules/no-nesting.md) | Disallow nested `then()` or `catch()` statements. | | ✅ | | |
| [no-new-statics](docs/rules/no-new-statics.md) | Disallow calling `new` on a Promise static method. | ✅ | | | 🔧 |
| [no-promise-in-callback](docs/rules/no-promise-in-callback.md) | Disallow using promises inside of callbacks. | | ✅ | | |
| [no-return-in-finally](docs/rules/no-return-in-finally.md) | Disallow return statements in `finally()`. | | ✅ | | |
| [no-return-wrap](docs/rules/no-return-wrap.md) | Disallow wrapping values in `Promise.resolve` or `Promise.reject` when not needed. | ✅ | | | |
| [param-names](docs/rules/param-names.md) | Enforce consistent param names and ordering when creating new promises. | ✅ | | | |
| [prefer-await-to-callbacks](docs/rules/prefer-await-to-callbacks.md) | Prefer async/await to the callback pattern. | | | | |
| [prefer-await-to-then](docs/rules/prefer-await-to-then.md) | Prefer `await` to `then()`/`catch()`/`finally()` for reading Promise values. | | | | |
| [valid-params](docs/rules/valid-params.md) | Enforces the proper number of arguments are passed to Promise functions. | | ✅ | | |
<!-- end auto-generated rules list -->
## Maintainers
- Jamund Ferguson - [@xjamundx][]
- Macklin Underdown - [@macklinu][]
- Aadit M Shah - [@aaditmshah][]
## License
- (c) MMXV jden <mailto:jason@denizac.org> - ISC license.
- (c) 2016 Jamund Ferguson <mailto:jamund@gmail.com> - ISC license.
[nodeify]: https://www.npmjs.com/package/nodeify
[pify]: https://www.npmjs.com/package/pify
[@aaditmshah]: https://github.com/aaditmshah
[@macklinu]: https://github.com/macklinu
[@xjamundx]: https://github.com/xjamundx

47
home/ags/node_modules/eslint-plugin-promise/index.js generated vendored Normal file
View file

@ -0,0 +1,47 @@
'use strict'
module.exports = {
rules: {
'param-names': require('./rules/param-names'),
'no-return-wrap': require('./rules/no-return-wrap'),
'always-return': require('./rules/always-return'),
'catch-or-return': require('./rules/catch-or-return'),
'prefer-await-to-callbacks': require('./rules/prefer-await-to-callbacks'),
'prefer-await-to-then': require('./rules/prefer-await-to-then'),
'no-native': require('./rules/no-native'),
'no-callback-in-promise': require('./rules/no-callback-in-promise'),
'no-promise-in-callback': require('./rules/no-promise-in-callback'),
'no-nesting': require('./rules/no-nesting'),
'avoid-new': require('./rules/avoid-new'),
'no-new-statics': require('./rules/no-new-statics'),
'no-return-in-finally': require('./rules/no-return-in-finally'),
'valid-params': require('./rules/valid-params'),
'no-multiple-resolved': require('./rules/no-multiple-resolved'),
},
rulesConfig: {
'param-names': 1,
'always-return': 1,
'no-return-wrap': 1,
'no-native': 0,
'catch-or-return': 1,
},
configs: {
recommended: {
plugins: ['promise'],
rules: {
'promise/always-return': 'error',
'promise/no-return-wrap': 'error',
'promise/param-names': 'error',
'promise/catch-or-return': 'error',
'promise/no-native': 'off',
'promise/no-nesting': 'warn',
'promise/no-promise-in-callback': 'warn',
'promise/no-callback-in-promise': 'warn',
'promise/avoid-new': 'off',
'promise/no-new-statics': 'error',
'promise/no-return-in-finally': 'warn',
'promise/valid-params': 'warn',
},
},
},
}

View file

@ -0,0 +1,93 @@
{
"name": "eslint-plugin-promise",
"version": "6.2.0",
"description": "Enforce best practices for JavaScript promises",
"keywords": [
"eslint",
"eslintplugin",
"eslint-plugin",
"promise",
"promises"
],
"homepage": "https://github.com/eslint-community/eslint-plugin-promise",
"bugs": "https://github.com/eslint-community/eslint-plugin-promise/issues",
"repository": {
"type": "git",
"url": "https://github.com/eslint-community/eslint-plugin-promise"
},
"license": "ISC",
"author": "jden <jason@denizac.org>",
"contributors": [
"Brett Zamir",
"Aadit M Shah <aaditmshah@aadit.codes> (https://aadit.codes/)"
],
"scripts": {
"format": "prettier --write .",
"lint": "npm-run-all \"lint:*\"",
"lint:eslint-docs": "npm run update:eslint-docs && git diff --exit-code",
"lint:js": "eslint --report-unused-disable-directives .",
"prepare": "husky install",
"test": "jest --coverage",
"update:eslint-docs": "eslint-doc-generator && npm run format"
},
"lint-staged": {
"{README.md,CONTRIBUTING.md}": [
"doctoc --maxlevel 3 --notitle"
],
"*.js": [
"prettier --write",
"eslint --report-unused-disable-directives --fix"
],
"*.+(json|md)": [
"prettier --write"
]
},
"prettier": {
"proseWrap": "always",
"semi": false,
"singleQuote": true
},
"jest": {
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
},
"collectCoverageFrom": [
"rules/*.js",
"rules/*/*.js",
"!rules/lib/eslint-compat.js"
],
"testPathIgnorePatterns": [
"__tests__/rule-tester.js"
]
},
"devDependencies": {
"@typescript-eslint/parser": "^5.45.0",
"doctoc": "^2.2.1",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-doc-generator": "^0.25.0",
"eslint-plugin-eslint-plugin": "^4.4.1",
"eslint-plugin-jest": "^26.9.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"globals": "^14.0.0",
"husky": "^7.0.4",
"jest": "^28.1.3",
"lint-staged": "^12.5.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"typescript": "^4.9.3"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": "https://opencollective.com/eslint"
}

View file

@ -0,0 +1,253 @@
'use strict'
const getDocsUrl = require('./lib/get-docs-url')
/**
* @typedef {import('estree').Node} Node
* @typedef {import('estree').SimpleCallExpression} CallExpression
* @typedef {import('estree').FunctionExpression} FunctionExpression
* @typedef {import('estree').ArrowFunctionExpression} ArrowFunctionExpression
* @typedef {import('eslint').Rule.CodePath} CodePath
* @typedef {import('eslint').Rule.CodePathSegment} CodePathSegment
*/
/**
* @typedef { (FunctionExpression | ArrowFunctionExpression) & { parent: CallExpression }} InlineThenFunctionExpression
*/
/** @param {Node} node */
function isFunctionWithBlockStatement(node) {
if (node.type === 'FunctionExpression') {
return true
}
if (node.type === 'ArrowFunctionExpression') {
return node.body.type === 'BlockStatement'
}
return false
}
/**
* @param {string} memberName
* @param {Node} node
* @returns {node is CallExpression}
*/
function isMemberCall(memberName, node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
!node.callee.computed &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === memberName
)
}
/** @param {Node} node */
function isFirstArgument(node) {
return Boolean(
node.parent && node.parent.arguments && node.parent.arguments[0] === node
)
}
/**
* @param {Node} node
* @returns {node is InlineThenFunctionExpression}
*/
function isInlineThenFunctionExpression(node) {
return (
isFunctionWithBlockStatement(node) &&
isMemberCall('then', node.parent) &&
isFirstArgument(node)
)
}
/**
* Checks whether the given node is the last `then()` callback in a promise chain.
* @param {InlineThenFunctionExpression} node
*/
function isLastCallback(node) {
/** @type {Node} */
let target = node.parent
/** @type {Node | undefined} */
let parent = target.parent
while (parent) {
if (parent.type === 'ExpressionStatement') {
// e.g. { promise.then(() => value) }
return true
}
if (parent.type === 'UnaryExpression') {
// e.g. void promise.then(() => value)
return parent.operator === 'void'
}
/** @type {Node | null} */
let nextTarget = null
if (parent.type === 'SequenceExpression') {
if (peek(parent.expressions) !== target) {
// e.g. (promise?.then(() => value), expr)
return true
}
nextTarget = parent
} else if (
// e.g. promise?.then(() => value)
parent.type === 'ChainExpression' ||
// e.g. await promise.then(() => value)
parent.type === 'AwaitExpression'
) {
nextTarget = parent
} else if (parent.type === 'MemberExpression') {
if (
parent.parent &&
(isMemberCall('catch', parent.parent) ||
isMemberCall('finally', parent.parent))
) {
// e.g. promise.then(() => value).catch(e => {})
nextTarget = parent.parent
}
}
if (nextTarget) {
target = nextTarget
parent = target.parent
continue
}
return false
}
// istanbul ignore next
return false
}
/**
* @template T
* @param {T[]} arr
* @returns {T}
*/
function peek(arr) {
return arr[arr.length - 1]
}
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Require returning inside each `then()` to create readable and reusable Promise chains.',
url: getDocsUrl('always-return'),
},
schema: [
{
type: 'object',
properties: {
ignoreLastCallback: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
},
create(context) {
const options = context.options[0] || {}
const ignoreLastCallback = !!options.ignoreLastCallback
/**
* @typedef {object} FuncInfo
* @property {string[]} branchIDStack This is a stack representing the currently
* executing branches ("codePathSegment"s) within the given function
* @property {Record<string, BranchInfo | undefined>} branchInfoMap This is an object representing information
* about all branches within the given function
*
* @typedef {object} BranchInfo
* @property {boolean} good This is a boolean representing whether
* the given branch explicitly `return`s or `throw`s. It starts as `false`
* for every branch and is updated to `true` if a `return` or `throw`
* statement is found
* @property {Node} node This is a estree Node object
* for the given branch
*/
/**
* funcInfoStack is a stack representing the stack of currently executing
* functions
* example:
* funcInfoStack = [ { branchIDStack: [ 's1_1' ],
* branchInfoMap:
* { s1_1:
* { good: false,
* loc: <loc> } } },
* { branchIDStack: ['s2_1', 's2_4'],
* branchInfoMap:
* { s2_1:
* { good: false,
* loc: <loc> },
* s2_2:
* { good: true,
* loc: <loc> },
* s2_4:
* { good: false,
* loc: <loc> } } } ]
* @type {FuncInfo[]}
*/
const funcInfoStack = []
function markCurrentBranchAsGood() {
const funcInfo = peek(funcInfoStack)
const currentBranchID = peek(funcInfo.branchIDStack)
if (funcInfo.branchInfoMap[currentBranchID]) {
funcInfo.branchInfoMap[currentBranchID].good = true
}
// else unreachable code
}
return {
'ReturnStatement:exit': markCurrentBranchAsGood,
'ThrowStatement:exit': markCurrentBranchAsGood,
/**
* @param {CodePathSegment} segment
* @param {Node} node
*/
onCodePathSegmentStart(segment, node) {
const funcInfo = peek(funcInfoStack)
funcInfo.branchIDStack.push(segment.id)
funcInfo.branchInfoMap[segment.id] = { good: false, node }
},
onCodePathSegmentEnd() {
const funcInfo = peek(funcInfoStack)
funcInfo.branchIDStack.pop()
},
onCodePathStart() {
funcInfoStack.push({
branchIDStack: [],
branchInfoMap: {},
})
},
/**
* @param {CodePath} path
* @param {Node} node
*/
onCodePathEnd(path, node) {
const funcInfo = funcInfoStack.pop()
if (!isInlineThenFunctionExpression(node)) {
return
}
if (ignoreLastCallback && isLastCallback(node)) {
return
}
path.finalSegments.forEach((segment) => {
const id = segment.id
const branch = funcInfo.branchInfoMap[id]
if (!branch.good) {
context.report({
message: 'Each then() should return a value or throw',
node: branch.node,
})
}
})
},
}
},
}

View file

@ -0,0 +1,29 @@
/**
* Rule: avoid-new
* Avoid creating new promises outside of utility libraries.
*/
'use strict'
const getDocsUrl = require('./lib/get-docs-url')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow creating `new` promises outside of utility libs (use [pify][] instead).',
url: getDocsUrl('avoid-new'),
},
schema: [],
},
create(context) {
return {
NewExpression(node) {
if (node.callee.name === 'Promise') {
context.report({ node, message: 'Avoid creating new promises.' })
}
},
}
},
}

View file

@ -0,0 +1,122 @@
/**
* Rule: catch-or-return
* Ensures that promises either include a catch() handler
* or are returned (to be handled upstream)
*/
'use strict'
const getDocsUrl = require('./lib/get-docs-url')
const isPromise = require('./lib/is-promise')
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce the use of `catch()` on un-returned promises.',
url: getDocsUrl('catch-or-return'),
},
messages: {
terminationMethod: 'Expected {{ terminationMethod }}() or return',
},
schema: [
{
type: 'object',
properties: {
allowFinally: {
type: 'boolean',
},
allowThen: {
type: 'boolean',
},
terminationMethod: {
oneOf: [
{ type: 'string' },
{
type: 'array',
items: {
type: 'string',
},
},
],
},
},
additionalProperties: false,
},
],
},
create(context) {
const options = context.options[0] || {}
const allowThen = options.allowThen
const allowFinally = options.allowFinally
let terminationMethod = options.terminationMethod || 'catch'
if (typeof terminationMethod === 'string') {
terminationMethod = [terminationMethod]
}
function isAllowedPromiseTermination(expression) {
// somePromise.then(a, b)
if (
allowThen &&
expression.type === 'CallExpression' &&
expression.callee.type === 'MemberExpression' &&
expression.callee.property.name === 'then' &&
expression.arguments.length === 2
) {
return true
}
// somePromise.catch().finally(fn)
if (
allowFinally &&
expression.type === 'CallExpression' &&
expression.callee.type === 'MemberExpression' &&
expression.callee.property.name === 'finally' &&
isPromise(expression.callee.object) &&
isAllowedPromiseTermination(expression.callee.object)
) {
return true
}
// somePromise.catch()
if (
expression.type === 'CallExpression' &&
expression.callee.type === 'MemberExpression' &&
terminationMethod.indexOf(expression.callee.property.name) !== -1
) {
return true
}
// somePromise['catch']()
if (
expression.type === 'CallExpression' &&
expression.callee.type === 'MemberExpression' &&
expression.callee.property.type === 'Literal' &&
expression.callee.property.value === 'catch'
) {
return true
}
return false
}
return {
ExpressionStatement(node) {
if (!isPromise(node.expression)) {
return
}
if (isAllowedPromiseTermination(node.expression)) {
return
}
context.report({
node,
messageId: 'terminationMethod',
data: { terminationMethod },
})
},
}
},
}

View file

@ -0,0 +1,33 @@
'use strict'
function getSourceCode(context) {
if (context.sourceCode != null) {
return context.sourceCode
}
return context.getSourceCode()
}
function getAncestors(context, node) {
const sourceCode = getSourceCode(context)
if (typeof sourceCode.getAncestors === 'function') {
return sourceCode.getAncestors(node)
}
return context.getAncestors(node)
}
function getScope(context, node) {
const sourceCode = getSourceCode(context)
if (typeof sourceCode.getScope === 'function') {
return sourceCode.getScope(node)
}
return context.getScope(node)
}
module.exports = {
getSourceCode,
getAncestors,
getScope,
}

View file

@ -0,0 +1,17 @@
'use strict'
const REPO_URL = 'https://github.com/eslint-community/eslint-plugin-promise'
/**
* Generates the URL to documentation for the given rule name. It uses the
* package version to build the link to a tagged version of the
* documentation file.
*
* @param {string} ruleName - Name of the eslint rule
* @returns {string} URL to the documentation for the given rule
*/
function getDocsUrl(ruleName) {
return `${REPO_URL}/blob/main/docs/rules/${ruleName}.md`
}
module.exports = getDocsUrl

View file

@ -0,0 +1,37 @@
/**
* Library: Has Promise Callback
* Makes sure that an Expression node is part of a promise
* with callback functions (like then() or catch())
*/
'use strict'
/**
* @typedef {import('estree').SimpleCallExpression} CallExpression
* @typedef {import('estree').MemberExpression} MemberExpression
* @typedef {import('estree').Identifier} Identifier
*
* @typedef {object} NameIsThenOrCatch
* @property {'then' | 'catch'} name
*
* @typedef {object} PropertyIsThenOrCatch
* @property {Identifier & NameIsThenOrCatch} property
*
* @typedef {object} CalleeIsPromiseCallback
* @property {MemberExpression & PropertyIsThenOrCatch} callee
*
* @typedef {CallExpression & CalleeIsPromiseCallback} HasPromiseCallback
*/
/**
* @param {import('estree').Node} node
* @returns {node is HasPromiseCallback}
*/
function hasPromiseCallback(node) {
// istanbul ignore if -- only being called within `CallExpression`
if (node.type !== 'CallExpression') return
if (node.callee.type !== 'MemberExpression') return
const propertyName = node.callee.property.name
return propertyName === 'then' || propertyName === 'catch'
}
module.exports = hasPromiseCallback

View file

@ -0,0 +1,14 @@
'use strict'
const isNamedCallback = require('./is-named-callback')
function isCallback(node, exceptions) {
const isCallExpression = node.type === 'CallExpression'
// istanbul ignore next -- always invoked on `CallExpression`
const callee = node.callee || {}
const nameIsCallback = isNamedCallback(callee.name, exceptions)
const isCB = isCallExpression && nameIsCallback
return isCB
}
module.exports = isCallback

View file

@ -0,0 +1,20 @@
'use strict'
const isInsidePromise = require('./is-inside-promise')
function isInsideCallback(node) {
const isCallExpression =
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionDeclaration' // this may be controversial
// it's totally fine to use promises inside promises
if (isInsidePromise(node)) return
const name = node.params && node.params[0] && node.params[0].name
const firstArgIsError = name === 'err' || name === 'error'
const isInACallback = isCallExpression && firstArgIsError
return isInACallback
}
module.exports = isInsideCallback

View file

@ -0,0 +1,15 @@
'use strict'
function isInsidePromise(node) {
const isFunctionExpression =
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
const parent = node.parent || {}
const callee = parent.callee || {}
const name = (callee.property && callee.property.name) || ''
const parentIsPromise = name === 'then' || name === 'catch'
const isInCB = isFunctionExpression && parentIsPromise
return isInCB
}
module.exports = isInsidePromise

View file

@ -0,0 +1,14 @@
'use strict'
let callbacks = ['done', 'cb', 'callback', 'next']
module.exports = function isNamedCallback(potentialCallbackName, exceptions) {
for (let i = 0; i < exceptions.length; i++) {
callbacks = callbacks.filter((item) => {
return item !== exceptions[i]
})
}
return callbacks.some((trueCallbackName) => {
return potentialCallbackName === trueCallbackName
})
}

View file

@ -0,0 +1,48 @@
/**
* Library: isPromiseConstructor
* Makes sure that an Expression node is new Promise().
*/
'use strict'
/**
* @typedef {import('estree').Node} Node
* @typedef {import('estree').Expression} Expression
* @typedef {import('estree').NewExpression} NewExpression
* @typedef {import('estree').FunctionExpression} FunctionExpression
* @typedef {import('estree').ArrowFunctionExpression} ArrowFunctionExpression
*
* @typedef {NewExpression & { callee: { type: 'Identifier', name: 'Promise' } }} NewPromise
* @typedef {NewPromise & { arguments: [FunctionExpression | ArrowFunctionExpression] }} NewPromiseWithInlineExecutor
*
*/
/**
* Checks whether the given node is new Promise().
* @param {Node} node
* @returns {node is NewPromise}
*/
function isPromiseConstructor(node) {
return (
node.type === 'NewExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'Promise'
)
}
/**
* Checks whether the given node is new Promise(() => {}).
* @param {Node} node
* @returns {node is NewPromiseWithInlineExecutor}
*/
function isPromiseConstructorWithInlineExecutor(node) {
return (
isPromiseConstructor(node) &&
node.arguments.length === 1 &&
(node.arguments[0].type === 'FunctionExpression' ||
node.arguments[0].type === 'ArrowFunctionExpression')
)
}
module.exports = {
isPromiseConstructor,
isPromiseConstructorWithInlineExecutor,
}

View file

@ -0,0 +1,36 @@
/**
* Library: isPromise
* Makes sure that an Expression node is part of a promise.
*/
'use strict'
const PROMISE_STATICS = require('./promise-statics')
function isPromise(expression) {
return (
// hello.then()
(expression.type === 'CallExpression' &&
expression.callee.type === 'MemberExpression' &&
expression.callee.property.name === 'then') ||
// hello.catch()
(expression.type === 'CallExpression' &&
expression.callee.type === 'MemberExpression' &&
expression.callee.property.name === 'catch') ||
// hello.finally()
(expression.type === 'CallExpression' &&
expression.callee.type === 'MemberExpression' &&
expression.callee.property.name === 'finally') ||
// somePromise.ANYTHING()
(expression.type === 'CallExpression' &&
expression.callee.type === 'MemberExpression' &&
isPromise(expression.callee.object)) ||
// Promise.STATIC_METHOD()
(expression.type === 'CallExpression' &&
expression.callee.type === 'MemberExpression' &&
expression.callee.object.type === 'Identifier' &&
expression.callee.object.name === 'Promise' &&
PROMISE_STATICS[expression.callee.property.name])
)
}
module.exports = isPromise

View file

@ -0,0 +1,10 @@
'use strict'
module.exports = {
all: true,
allSettled: true,
any: true,
race: true,
reject: true,
resolve: true,
}

View file

@ -0,0 +1,71 @@
/**
* Rule: no-callback-in-promise
* Avoid calling back inside of a promise
*/
'use strict'
const { getAncestors } = require('./lib/eslint-compat')
const getDocsUrl = require('./lib/get-docs-url')
const hasPromiseCallback = require('./lib/has-promise-callback')
const isInsidePromise = require('./lib/is-inside-promise')
const isCallback = require('./lib/is-callback')
const CB_BLACKLIST = ['callback', 'cb', 'next', 'done']
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow calling `cb()` inside of a `then()` (use [nodeify][] instead).',
url: getDocsUrl('no-callback-in-promise'),
},
messages: {
callback: 'Avoid calling back inside of a promise.',
},
schema: [
{
type: 'object',
properties: {
exceptions: {
type: 'array',
items: {
type: 'string',
},
},
},
additionalProperties: false,
},
],
},
create(context) {
return {
CallExpression(node) {
const options = context.options[0] || {}
const exceptions = options.exceptions || []
if (!isCallback(node, exceptions)) {
// in general we send you packing if you're not a callback
// but we also need to watch out for whatever.then(cb)
if (hasPromiseCallback(node)) {
const name =
node.arguments && node.arguments[0] && node.arguments[0].name
if (!exceptions.includes(name) && CB_BLACKLIST.includes(name)) {
context.report({
node: node.arguments[0],
messageId: 'callback',
})
}
}
return
}
if (getAncestors(context, node).some(isInsidePromise)) {
context.report({
node,
messageId: 'callback',
})
}
},
}
},
}

View file

@ -0,0 +1,476 @@
/**
* Rule: no-multiple-resolved
* Disallow creating new promises with paths that resolve multiple times
*/
'use strict'
const { getScope } = require('./lib/eslint-compat')
const getDocsUrl = require('./lib/get-docs-url')
const {
isPromiseConstructorWithInlineExecutor,
} = require('./lib/is-promise-constructor')
/**
* @typedef {import('estree').Node} Node
* @typedef {import('estree').Expression} Expression
* @typedef {import('estree').Identifier} Identifier
* @typedef {import('estree').FunctionExpression} FunctionExpression
* @typedef {import('estree').ArrowFunctionExpression} ArrowFunctionExpression
* @typedef {import('estree').SimpleCallExpression} CallExpression
* @typedef {import('estree').MemberExpression} MemberExpression
* @typedef {import('estree').NewExpression} NewExpression
* @typedef {import('estree').ImportExpression} ImportExpression
* @typedef {import('estree').YieldExpression} YieldExpression
* @typedef {import('eslint').Rule.CodePath} CodePath
* @typedef {import('eslint').Rule.CodePathSegment} CodePathSegment
*/
/**
* An expression that can throw an error.
* see https://github.com/eslint/eslint/blob/e940be7a83d0caea15b64c1e1c2785a6540e2641/lib/linter/code-path-analysis/code-path-analyzer.js#L639-L643
* @typedef {CallExpression | MemberExpression | NewExpression | ImportExpression | YieldExpression} ThrowableExpression
*/
/**
* Iterate all previous path segments.
* @param {CodePathSegment} segment
* @returns {Iterable<CodePathSegment[]>}
*/
function* iterateAllPrevPathSegments(segment) {
yield* iterate(segment, [])
/**
* @param {CodePathSegment} segment
* @param {CodePathSegment[]} processed
*/
function* iterate(segment, processed) {
if (processed.includes(segment)) {
return
}
const nextProcessed = [segment, ...processed]
for (const prev of segment.prevSegments) {
if (prev.prevSegments.length === 0) {
yield [prev]
} else {
for (const segments of iterate(prev, nextProcessed)) {
yield [prev, ...segments]
}
}
}
}
}
/**
* Iterate all next path segments.
* @param {CodePathSegment} segment
* @returns {Iterable<CodePathSegment[]>}
*/
function* iterateAllNextPathSegments(segment) {
yield* iterate(segment, [])
/**
* @param {CodePathSegment} segment
* @param {CodePathSegment[]} processed
*/
function* iterate(segment, processed) {
if (processed.includes(segment)) {
return
}
const nextProcessed = [segment, ...processed]
for (const next of segment.nextSegments) {
if (next.nextSegments.length === 0) {
yield [next]
} else {
for (const segments of iterate(next, nextProcessed)) {
yield [next, ...segments]
}
}
}
}
}
/**
* Finds the same route path from the given path following previous path segments.
* @param {CodePathSegment} segment
* @returns {CodePathSegment | null}
*/
function findSameRoutePathSegment(segment) {
/** @type {Set<CodePathSegment>} */
const routeSegments = new Set()
for (const route of iterateAllPrevPathSegments(segment)) {
if (routeSegments.size === 0) {
// First
for (const seg of route) {
routeSegments.add(seg)
}
continue
}
for (const seg of routeSegments) {
if (!route.includes(seg)) {
routeSegments.delete(seg)
}
}
}
for (const routeSegment of routeSegments) {
let hasUnreached = false
for (const segments of iterateAllNextPathSegments(routeSegment)) {
if (!segments.includes(segment)) {
// It has a route that does not reach the given path.
hasUnreached = true
break
}
}
if (!hasUnreached) {
return routeSegment
}
}
return null
}
class CodePathInfo {
/**
* @param {CodePath} path
*/
constructor(path) {
this.path = path
/** @type {Map<CodePathSegment, CodePathSegmentInfo>} */
this.segmentInfos = new Map()
this.resolvedCount = 0
/** @type {CodePathSegment[]} */
this.allSegments = []
}
getCurrentSegmentInfos() {
return this.path.currentSegments.map((segment) => {
const info = this.segmentInfos.get(segment)
if (info) {
return info
}
const newInfo = new CodePathSegmentInfo(this, segment)
this.segmentInfos.set(segment, newInfo)
return newInfo
})
}
/**
* @typedef {object} AlreadyResolvedData
* @property {Identifier} resolved
* @property {'certain' | 'potential'} kind
*/
/**
* Check all paths and return paths resolved multiple times.
* @param {PromiseCodePathContext} promiseCodePathContext
* @returns {Iterable<AlreadyResolvedData & { node: Identifier }>}
*/
*iterateReports(promiseCodePathContext) {
const targets = [...this.segmentInfos.values()].filter(
(info) => info.resolved
)
for (const segmentInfo of targets) {
const result = this._getAlreadyResolvedData(
segmentInfo.segment,
promiseCodePathContext
)
if (result) {
yield {
node: segmentInfo.resolved,
resolved: result.resolved,
kind: result.kind,
}
}
}
}
/**
* Compute the previously resolved path.
* @param {CodePathSegment} segment
* @param {PromiseCodePathContext} promiseCodePathContext
* @returns {AlreadyResolvedData | null}
*/
_getAlreadyResolvedData(segment, promiseCodePathContext) {
const prevSegments = segment.prevSegments.filter(
(prev) => !promiseCodePathContext.isResolvedTryBlockCodePathSegment(prev)
)
if (prevSegments.length === 0) {
return null
}
const prevSegmentInfos = prevSegments.map((prev) =>
this._getProcessedSegmentInfo(prev, promiseCodePathContext)
)
if (prevSegmentInfos.every((info) => info.resolved)) {
// If the previous paths are all resolved, the next path is also resolved.
return {
resolved: prevSegmentInfos[0].resolved,
kind: 'certain',
}
}
for (const prevSegmentInfo of prevSegmentInfos) {
if (prevSegmentInfo.resolved) {
// If the previous path is partially resolved,
// then the next path is potentially resolved.
return {
resolved: prevSegmentInfo.resolved,
kind: 'potential',
}
}
if (prevSegmentInfo.potentiallyResolved) {
let potential = false
if (prevSegmentInfo.segment.nextSegments.length === 1) {
// If the previous path is potentially resolved and there is one next path,
// then the next path is potentially resolved.
potential = true
} else {
// This is necessary, for example, if `resolve()` in the finally section.
const segmentInfo = this.segmentInfos.get(segment)
if (segmentInfo && segmentInfo.resolved) {
if (
prevSegmentInfo.segment.nextSegments.every((next) => {
const nextSegmentInfo = this.segmentInfos.get(next)
return (
nextSegmentInfo &&
nextSegmentInfo.resolved === segmentInfo.resolved
)
})
) {
// If the previous path is potentially resolved and
// the next paths all point to the same resolved node,
// then the next path is potentially resolved.
potential = true
}
}
}
if (potential) {
return {
resolved: prevSegmentInfo.potentiallyResolved,
kind: 'potential',
}
}
}
}
const sameRoute = findSameRoutePathSegment(segment)
if (sameRoute) {
const sameRouteSegmentInfo = this._getProcessedSegmentInfo(sameRoute)
if (sameRouteSegmentInfo.potentiallyResolved) {
return {
resolved: sameRouteSegmentInfo.potentiallyResolved,
kind: 'potential',
}
}
}
return null
}
/**
* @param {CodePathSegment} segment
* @param {PromiseCodePathContext} promiseCodePathContext
*/
_getProcessedSegmentInfo(segment, promiseCodePathContext) {
const segmentInfo = this.segmentInfos.get(segment)
if (segmentInfo) {
return segmentInfo
}
const newInfo = new CodePathSegmentInfo(this, segment)
this.segmentInfos.set(segment, newInfo)
const alreadyResolvedData = this._getAlreadyResolvedData(
segment,
promiseCodePathContext
)
if (alreadyResolvedData) {
if (alreadyResolvedData.kind === 'certain') {
newInfo.resolved = alreadyResolvedData.resolved
} else {
newInfo.potentiallyResolved = alreadyResolvedData.resolved
}
}
return newInfo
}
}
class CodePathSegmentInfo {
/**
* @param {CodePathInfo} pathInfo
* @param {CodePathSegment} segment
*/
constructor(pathInfo, segment) {
this.pathInfo = pathInfo
this.segment = segment
/** @type {Identifier | null} */
this._resolved = null
/** @type {Identifier | null} */
this.potentiallyResolved = null
}
get resolved() {
return this._resolved
}
/** @type {Identifier} */
set resolved(identifier) {
this._resolved = identifier
this.pathInfo.resolvedCount++
}
}
class PromiseCodePathContext {
constructor() {
/** @type {Set<string>} */
this.resolvedSegmentIds = new Set()
}
/** @param {CodePathSegment} */
addResolvedTryBlockCodePathSegment(segment) {
this.resolvedSegmentIds.add(segment.id)
}
/** @param {CodePathSegment} */
isResolvedTryBlockCodePathSegment(segment) {
return this.resolvedSegmentIds.has(segment.id)
}
}
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Disallow creating new promises with paths that resolve multiple times.',
url: getDocsUrl('no-multiple-resolved'),
},
messages: {
alreadyResolved:
'Promise should not be resolved multiple times. Promise is already resolved on line {{line}}.',
potentiallyAlreadyResolved:
'Promise should not be resolved multiple times. Promise is potentially resolved on line {{line}}.',
},
schema: [],
},
/** @param {import('eslint').Rule.RuleContext} context */
create(context) {
const reported = new Set()
const promiseCodePathContext = new PromiseCodePathContext()
/**
* @param {Identifier} node
* @param {Identifier} resolved
* @param {'certain' | 'potential'} kind
*/
function report(node, resolved, kind) {
if (reported.has(node)) {
return
}
reported.add(node)
context.report({
node: node.parent,
messageId:
kind === 'certain' ? 'alreadyResolved' : 'potentiallyAlreadyResolved',
data: {
line: resolved.loc.start.line,
},
})
}
/**
* @param {CodePathInfo} codePathInfo
* @param {PromiseCodePathContext} promiseCodePathContext
*/
function verifyMultipleResolvedPath(codePathInfo, promiseCodePathContext) {
for (const { node, resolved, kind } of codePathInfo.iterateReports(
promiseCodePathContext
)) {
report(node, resolved, kind)
}
}
/** @type {CodePathInfo[]} */
const codePathInfoStack = []
/** @type {Set<Identifier>[]} */
const resolverReferencesStack = [new Set()]
/** @type {ThrowableExpression | null} */
let lastThrowableExpression = null
return {
/** @param {FunctionExpression | ArrowFunctionExpression} node */
'FunctionExpression, ArrowFunctionExpression'(node) {
if (!isPromiseConstructorWithInlineExecutor(node.parent)) {
return
}
// Collect and stack `resolve` and `reject` references.
/** @type {Set<Identifier>} */
const resolverReferences = new Set()
const resolvers = node.params.filter(
/** @returns {node is Identifier} */
(node) => node && node.type === 'Identifier'
)
for (const resolver of resolvers) {
const variable = getScope(context, node).set.get(resolver.name)
// istanbul ignore next -- Usually always present.
if (!variable) continue
for (const reference of variable.references) {
resolverReferences.add(reference.identifier)
}
}
resolverReferencesStack.unshift(resolverReferences)
},
/** @param {FunctionExpression | ArrowFunctionExpression} node */
'FunctionExpression, ArrowFunctionExpression:exit'(node) {
if (!isPromiseConstructorWithInlineExecutor(node.parent)) {
return
}
resolverReferencesStack.shift()
},
/** @param {CodePath} path */
onCodePathStart(path) {
codePathInfoStack.unshift(new CodePathInfo(path))
},
onCodePathEnd() {
const codePathInfo = codePathInfoStack.shift()
if (codePathInfo.resolvedCount > 1) {
verifyMultipleResolvedPath(codePathInfo, promiseCodePathContext)
}
},
/** @param {ThrowableExpression} node */
'CallExpression, MemberExpression, NewExpression, ImportExpression, YieldExpression:exit'(
node
) {
lastThrowableExpression = node
},
/**
* @param {CodePathSegment} segment
* @param {Node} node
*/
onCodePathSegmentEnd(segment, node) {
if (
node.type === 'CatchClause' &&
lastThrowableExpression &&
lastThrowableExpression.type === 'CallExpression' &&
node.parent.type === 'TryStatement' &&
node.parent.range[0] <= lastThrowableExpression.range[0] &&
lastThrowableExpression.range[1] <= node.parent.range[1]
) {
const resolverReferences = resolverReferencesStack[0]
if (resolverReferences.has(lastThrowableExpression.callee)) {
// Mark a segment if the last expression in the try block is a call to resolve.
promiseCodePathContext.addResolvedTryBlockCodePathSegment(segment)
}
}
},
/** @type {Identifier} */
'CallExpression > Identifier.callee'(node) {
const codePathInfo = codePathInfoStack[0]
const resolverReferences = resolverReferencesStack[0]
if (!resolverReferences.has(node)) {
return
}
for (const segmentInfo of codePathInfo.getCurrentSegmentInfos()) {
// If a resolving path is found, report if the path is already resolved.
// Store the information if it is not already resolved.
if (segmentInfo.resolved) {
report(node, segmentInfo.resolved, 'certain')
continue
}
segmentInfo.resolved = node
}
},
}
},
}

View file

@ -0,0 +1,77 @@
// Borrowed from here:
// https://github.com/colonyamerican/eslint-plugin-cah/issues/3
'use strict'
const { getScope } = require('./lib/eslint-compat')
const getDocsUrl = require('./lib/get-docs-url')
function isDeclared(scope, ref) {
return scope.variables.some((variable) => {
if (variable.name !== ref.identifier.name) {
return false
}
// Presumably can't pass this since the implicit `Promise` global
// being checked here would always lack `defs`
// istanbul ignore else
if (!variable.defs || !variable.defs.length) {
return false
}
// istanbul ignore next
return true
})
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Require creating a `Promise` constructor before using it in an ES5 environment.',
url: getDocsUrl('no-native'),
},
messages: {
name: '"{{name}}" is not defined.',
},
schema: [],
},
create(context) {
/**
* Checks for and reports reassigned constants
*
* @param {Scope} scope - an eslint-scope Scope object
* @returns {void}
* @private
*/
return {
'Program:exit'(node) {
const scope = getScope(context, node)
const leftToBeResolved =
scope.implicit.left ||
/**
* Fixes https://github.com/eslint-community/eslint-plugin-promise/issues/205.
* The problem was that @typescript-eslint has a scope manager
* which has `leftToBeResolved` instead of the default `left`.
*/
scope.implicit.leftToBeResolved
leftToBeResolved.forEach((ref) => {
if (ref.identifier.name !== 'Promise') {
return
}
// istanbul ignore else
if (!isDeclared(scope, ref)) {
context.report({
node: ref.identifier,
messageId: 'name',
data: { name: ref.identifier.name },
})
}
})
},
}
},
}

View file

@ -0,0 +1,120 @@
/**
* Rule: no-nesting
* Avoid nesting your promises.
*/
'use strict'
const { getScope } = require('./lib/eslint-compat')
const getDocsUrl = require('./lib/get-docs-url')
const hasPromiseCallback = require('./lib/has-promise-callback')
const isInsidePromise = require('./lib/is-inside-promise')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow nested `then()` or `catch()` statements.',
url: getDocsUrl('no-nesting'),
},
schema: [],
},
create(context) {
/**
* Array of callback function scopes.
* Scopes are in order closest to the current node.
* @type {import('eslint').Scope.Scope[]}
*/
const callbackScopes = []
/**
* @param {import('eslint').Scope.Scope} scope
* @returns {Iterable<import('eslint').Scope.Reference>}
*/
function* iterateDefinedReferences(scope) {
for (const variable of scope.variables) {
for (const reference of variable.references) {
yield reference
}
}
}
return {
':function'(node) {
if (isInsidePromise(node)) {
callbackScopes.unshift(getScope(context, node))
}
},
':function:exit'(node) {
if (isInsidePromise(node)) {
callbackScopes.shift()
}
},
CallExpression(node) {
if (!hasPromiseCallback(node)) return
if (!callbackScopes.length) {
// The node is not in the callback function.
return
}
// Checks if the argument callback uses variables defined in the closest callback function scope.
//
// e.g.
// ```
// doThing()
// .then(a => getB(a)
// .then(b => getC(a, b))
// )
// ```
//
// In the above case, Since the variables it references are undef,
// we cannot refactor the nesting like following:
// ```
// doThing()
// .then(a => getB(a))
// .then(b => getC(a, b))
// ```
//
// However, `getD` can be refactored in the following:
// ```
// doThing()
// .then(a => getB(a)
// .then(b => getC(a, b)
// .then(c => getD(a, c))
// )
// )
// ```
// ↓
// ```
// doThing()
// .then(a => getB(a)
// .then(b => getC(a, b))
// .then(c => getD(a, c))
// )
// ```
// This is why we only check the closest callback function scope.
//
const closestCallbackScope = callbackScopes[0]
for (const reference of iterateDefinedReferences(
closestCallbackScope
)) {
if (
node.arguments.some(
(arg) =>
arg.range[0] <= reference.identifier.range[0] &&
reference.identifier.range[1] <= arg.range[1]
)
) {
// Argument callbacks refer to variables defined in the callback function.
return
}
}
context.report({
node: node.callee.property,
message: 'Avoid nesting promises.',
})
},
}
},
}

View file

@ -0,0 +1,39 @@
'use strict'
const PROMISE_STATICS = require('./lib/promise-statics')
const getDocsUrl = require('./lib/get-docs-url')
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow calling `new` on a Promise static method.',
url: getDocsUrl('no-new-statics'),
},
fixable: 'code',
schema: [],
},
create(context) {
return {
NewExpression(node) {
if (
node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'Promise' &&
PROMISE_STATICS[node.callee.property.name]
) {
context.report({
node,
message: "Avoid calling 'new' on 'Promise.{{ name }}()'",
data: { name: node.callee.property.name },
fix(fixer) {
return fixer.replaceTextRange(
[node.range[0], node.range[0] + 'new '.length],
''
)
},
})
}
},
}
},
}

View file

@ -0,0 +1,43 @@
/**
* Rule: no-promise-in-callback
* Discourage using promises inside of callbacks.
*/
'use strict'
const { getAncestors } = require('./lib/eslint-compat')
const getDocsUrl = require('./lib/get-docs-url')
const isPromise = require('./lib/is-promise')
const isInsideCallback = require('./lib/is-inside-callback')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow using promises inside of callbacks.',
url: getDocsUrl('no-promise-in-callback'),
},
schema: [],
},
create(context) {
return {
CallExpression(node) {
if (!isPromise(node)) return
// if i'm returning the promise, it's probably not really a callback
// function, and I should be okay....
if (node.parent.type === 'ReturnStatement') return
// what about if the parent is an ArrowFunctionExpression
// would that imply an implicit return?
if (getAncestors(context, node).some(isInsideCallback)) {
context.report({
node: node.callee,
message: 'Avoid using promises inside of callbacks.',
})
}
},
}
},
}

View file

@ -0,0 +1,47 @@
'use strict'
const getDocsUrl = require('./lib/get-docs-url')
const isPromise = require('./lib/is-promise')
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow return statements in `finally()`.',
url: getDocsUrl('no-return-in-finally'),
},
schema: [],
},
create(context) {
return {
CallExpression(node) {
if (isPromise(node)) {
if (
node.callee &&
node.callee.property &&
node.callee.property.name === 'finally'
) {
// istanbul ignore else -- passing `isPromise` means should have a body
if (
node.arguments &&
node.arguments[0] &&
node.arguments[0].body &&
node.arguments[0].body.body
) {
if (
node.arguments[0].body.body.some((statement) => {
return statement.type === 'ReturnStatement'
})
) {
context.report({
node: node.callee.property,
message: 'No return in finally',
})
}
}
}
}
},
}
},
}

View file

@ -0,0 +1,96 @@
/**
* Rule: no-return-wrap function
* Prevents unnecessary wrapping of results in Promise.resolve
* or Promise.reject as the Promise will do that for us
*/
'use strict'
const { getAncestors } = require('./lib/eslint-compat')
const getDocsUrl = require('./lib/get-docs-url')
const isPromise = require('./lib/is-promise')
function isInPromise(context, node) {
let functionNode = getAncestors(context, node)
.filter((node) => {
return (
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression'
)
})
.reverse()[0]
while (
functionNode &&
functionNode.parent &&
functionNode.parent.type === 'MemberExpression' &&
functionNode.parent.object === functionNode &&
functionNode.parent.property.type === 'Identifier' &&
functionNode.parent.property.name === 'bind' &&
functionNode.parent.parent &&
functionNode.parent.parent.type === 'CallExpression' &&
functionNode.parent.parent.callee === functionNode.parent
) {
functionNode = functionNode.parent.parent
}
return functionNode && functionNode.parent && isPromise(functionNode.parent)
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow wrapping values in `Promise.resolve` or `Promise.reject` when not needed.',
url: getDocsUrl('no-return-wrap'),
},
messages: {
resolve: 'Avoid wrapping return values in Promise.resolve',
reject: 'Expected throw instead of Promise.reject',
},
schema: [
{
type: 'object',
properties: {
allowReject: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
},
create(context) {
const options = context.options[0] || {}
const allowReject = options.allowReject
/**
* Checks a call expression, reporting if necessary.
* @param callExpression The call expression.
* @param node The node to report.
*/
function checkCallExpression({ callee }, node) {
if (
isInPromise(context, node) &&
callee.type === 'MemberExpression' &&
callee.object.name === 'Promise'
) {
if (callee.property.name === 'resolve') {
context.report({ node, messageId: 'resolve' })
} else if (!allowReject && callee.property.name === 'reject') {
context.report({ node, messageId: 'reject' })
}
}
}
return {
ReturnStatement(node) {
if (node.argument && node.argument.type === 'CallExpression') {
checkCallExpression(node.argument, node)
}
},
'ArrowFunctionExpression > CallExpression'(node) {
checkCallExpression(node, node)
},
}
},
}

View file

@ -0,0 +1,70 @@
'use strict'
const getDocsUrl = require('./lib/get-docs-url')
const {
isPromiseConstructorWithInlineExecutor,
} = require('./lib/is-promise-constructor')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Enforce consistent param names and ordering when creating new promises.',
url: getDocsUrl('param-names'),
},
schema: [
{
type: 'object',
properties: {
resolvePattern: { type: 'string' },
rejectPattern: { type: 'string' },
},
additionalProperties: false,
},
],
},
create(context) {
const options = context.options[0] || {}
const resolvePattern = new RegExp(
options.resolvePattern || '^_?resolve$',
'u'
)
const rejectPattern = new RegExp(options.rejectPattern || '^_?reject$', 'u')
return {
NewExpression(node) {
if (isPromiseConstructorWithInlineExecutor(node)) {
const params = node.arguments[0].params
if (!params || !params.length) {
return
}
const resolveParamName = params[0] && params[0].name
if (resolveParamName && !resolvePattern.test(resolveParamName)) {
context.report({
node: params[0],
message:
'Promise constructor parameters must be named to match "{{ resolvePattern }}"',
data: {
resolvePattern: resolvePattern.source,
},
})
}
const rejectParamName = params[1] && params[1].name
if (rejectParamName && !rejectPattern.test(rejectParamName)) {
context.report({
node: params[1],
message:
'Promise constructor parameters must be named to match "{{ rejectPattern }}"',
data: {
rejectPattern: rejectPattern.source,
},
})
}
}
},
}
},
}

View file

@ -0,0 +1,97 @@
'use strict'
const { getAncestors } = require('./lib/eslint-compat')
const getDocsUrl = require('./lib/get-docs-url')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Prefer async/await to the callback pattern.',
url: getDocsUrl('prefer-await-to-callbacks'),
},
messages: {
error: 'Avoid callbacks. Prefer Async/Await.',
},
schema: [],
},
create(context) {
function checkLastParamsForCallback(node) {
const lastParam = node.params[node.params.length - 1] || {}
if (lastParam.name === 'callback' || lastParam.name === 'cb') {
context.report({ node: lastParam, messageId: 'error' })
}
}
function isInsideYieldOrAwait(node) {
return getAncestors(context, node).some((parent) => {
return (
parent.type === 'AwaitExpression' || parent.type === 'YieldExpression'
)
})
}
return {
CallExpression(node) {
// Callbacks aren't allowed.
if (node.callee.name === 'cb' || node.callee.name === 'callback') {
context.report({ node, messageId: 'error' })
return
}
// Then-ables aren't allowed either.
const args = node.arguments
const lastArgIndex = args.length - 1
const arg = lastArgIndex > -1 && node.arguments[lastArgIndex]
if (
(arg && arg.type === 'FunctionExpression') ||
arg.type === 'ArrowFunctionExpression'
) {
// Ignore event listener callbacks.
if (
node.callee.property &&
(node.callee.property.name === 'on' ||
node.callee.property.name === 'once')
) {
return
}
// carve out exemption for map/filter/etc
const arrayMethods = [
'map',
'every',
'forEach',
'some',
'find',
'filter',
]
const isLodash =
node.callee.object &&
['lodash', 'underscore', '_'].includes(node.callee.object.name)
const callsArrayMethod =
node.callee.property &&
arrayMethods.includes(node.callee.property.name) &&
(node.arguments.length === 1 ||
(node.arguments.length === 2 && isLodash))
const isArrayMethod =
node.callee.name &&
arrayMethods.includes(node.callee.name) &&
node.arguments.length === 2
if (callsArrayMethod || isArrayMethod) return
// actually check for callbacks (I know this is the worst)
if (
arg.params &&
arg.params[0] &&
(arg.params[0].name === 'err' || arg.params[0].name === 'error')
) {
if (!isInsideYieldOrAwait(node)) {
context.report({ node: arg, messageId: 'error' })
}
}
}
},
FunctionDeclaration: checkLastParamsForCallback,
FunctionExpression: checkLastParamsForCallback,
ArrowFunctionExpression: checkLastParamsForCallback,
}
},
}

View file

@ -0,0 +1,61 @@
/**
* Rule: prefer-await-to-then
* Discourage using then()/catch()/finally() and instead use async/await.
*/
'use strict'
const { getAncestors, getScope } = require('./lib/eslint-compat')
const getDocsUrl = require('./lib/get-docs-url')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Prefer `await` to `then()`/`catch()`/`finally()` for reading Promise values.',
url: getDocsUrl('prefer-await-to-then'),
},
schema: [],
},
create(context) {
/** Returns true if node is inside yield or await expression. */
function isInsideYieldOrAwait(node) {
return getAncestors(context, node).some((parent) => {
return (
parent.type === 'AwaitExpression' || parent.type === 'YieldExpression'
)
})
}
/**
* Returns true if node is created at the top-level scope.
* Await statements are not allowed at the top level,
* only within function declarations.
*/
function isTopLevelScoped(node) {
return getScope(context, node).block.type === 'Program'
}
return {
'CallExpression > MemberExpression.callee'(node) {
if (isTopLevelScoped(node) || isInsideYieldOrAwait(node)) {
return
}
// if you're a then/catch/finally expression then you're probably a promise
if (
node.property &&
(node.property.name === 'then' ||
node.property.name === 'catch' ||
node.property.name === 'finally')
) {
context.report({
node: node.property,
message: 'Prefer await to then()/catch()/finally().',
})
}
},
}
},
}

View file

@ -0,0 +1,71 @@
'use strict'
const getDocsUrl = require('./lib/get-docs-url')
const isPromise = require('./lib/is-promise')
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Enforces the proper number of arguments are passed to Promise functions.',
url: getDocsUrl('valid-params'),
},
schema: [],
},
create(context) {
return {
CallExpression(node) {
if (!isPromise(node)) {
return
}
const name = node.callee.property.name
const numArgs = node.arguments.length
// istanbul ignore next -- `isPromise` filters out others
switch (name) {
case 'resolve':
case 'reject':
if (numArgs > 1) {
context.report({
node,
message:
'Promise.{{ name }}() requires 0 or 1 arguments, but received {{ numArgs }}',
data: { name, numArgs },
})
}
break
case 'then':
if (numArgs < 1 || numArgs > 2) {
context.report({
node,
message:
'Promise.{{ name }}() requires 1 or 2 arguments, but received {{ numArgs }}',
data: { name, numArgs },
})
}
break
case 'race':
case 'all':
case 'allSettled':
case 'any':
case 'catch':
case 'finally':
if (numArgs !== 1) {
context.report({
node,
message:
'Promise.{{ name }}() requires 1 argument, but received {{ numArgs }}',
data: { name, numArgs },
})
}
break
default:
// istanbul ignore next -- `isPromise` filters out others
break
}
},
}
},
}