├── .github ├── release-please │ ├── manifest.json │ └── config.json ├── renovate.json ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── compliance.yml │ ├── release-please.yml │ ├── format.yml │ └── ci.yml └── ISSUE_TEMPLATE.md ├── .husky └── pre-commit ├── .npmrc ├── .gitignore ├── .prettierignore ├── __tests__ ├── .eslintrc.json ├── index.js ├── avoid-new.js ├── no-return-in-finally.js ├── prefer-catch.js ├── no-native.js ├── rule-tester.js ├── param-names.js ├── no-new-statics.js ├── spec-only.js ├── prefer-await-to-callbacks.js ├── no-promise-in-callback.js ├── prefer-await-to-then.js ├── no-nesting.js ├── no-callback-in-promise.js ├── valid-params.js ├── no-return-wrap.js ├── always-return.js ├── catch-or-return.js └── no-multiple-resolved.js ├── rules ├── lib │ ├── promise-statics.js │ ├── is-named-callback.js │ ├── is-callback.js │ ├── is-inside-promise.js │ ├── get-docs-url.js │ ├── is-member-call-with-object-name.js │ ├── eslint-compat.js │ ├── is-inside-callback.js │ ├── has-promise-callback.js │ ├── parentheses.js │ ├── is-promise.js │ └── is-promise-constructor.js ├── avoid-new.js ├── no-new-statics.js ├── no-return-in-finally.js ├── fix │ └── remove-argument.js ├── no-promise-in-callback.js ├── prefer-catch.js ├── no-native.js ├── param-names.js ├── spec-only.js ├── valid-params.js ├── prefer-await-to-then.js ├── no-return-wrap.js ├── no-callback-in-promise.js ├── prefer-await-to-callbacks.js ├── no-nesting.js ├── catch-or-return.js └── always-return.js ├── .npmignore ├── .editorconfig ├── .eslint-doc-generatorrc.js ├── docs └── rules │ ├── spec-only.md │ ├── no-return-in-finally.md │ ├── no-nesting.md │ ├── no-native.md │ ├── no-multiple-resolved.md │ ├── prefer-catch.md │ ├── no-return-wrap.md │ ├── no-promise-in-callback.md │ ├── prefer-await-to-callbacks.md │ ├── no-new-statics.md │ ├── prefer-await-to-then.md │ ├── avoid-new.md │ ├── param-names.md │ ├── valid-params.md │ ├── always-return.md │ ├── no-callback-in-promise.md │ └── catch-or-return.md ├── LICENSE.md ├── .eslintrc.json ├── index.js ├── package.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md └── CHANGELOG.md /.github/release-please/manifest.json: -------------------------------------------------------------------------------- 1 | {".":"7.2.1"} 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npx lint-staged 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | scope= 2 | registry=https://registry.npmjs.org 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | coverage/ 4 | .eslintcache 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github/release-please/manifest.json 2 | coverage 3 | CHANGELOG.md 4 | -------------------------------------------------------------------------------- /__tests__/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest"], 3 | "extends": ["plugin:jest/recommended"], 4 | "env": { 5 | "jest/globals": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>eslint/eslint//.github/renovate.json5"] 4 | } 5 | -------------------------------------------------------------------------------- /rules/lib/promise-statics.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = new Set([ 4 | 'all', 5 | 'allSettled', 6 | 'any', 7 | 'race', 8 | 'reject', 9 | 'resolve', 10 | 'withResolvers', 11 | ]) 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | __tests__ 4 | .npmignore 5 | .eslintrc.json 6 | docs 7 | coverage 8 | .github 9 | CONTRIBUTING.md 10 | CODE_OF_CONDUCT.md 11 | .husky 12 | .editorconfig 13 | .prettierignore 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; EditorConfig file: https://EditorConfig.org 2 | ; Install the "EditorConfig" plugin into your editor to use 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **What is the purpose of this pull request?** 2 | 3 | - [ ] Documentation update 4 | - [ ] Bug fix 5 | - [ ] New rule 6 | - [ ] Changes an existing rule 7 | - [ ] Add autofixing to a rule 8 | - [ ] Other, please explain: 9 | 10 | **What changes did you make? (Give an overview)** 11 | -------------------------------------------------------------------------------- /.eslint-doc-generatorrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {import('eslint-doc-generator').GenerateOptions} */ 4 | module.exports = { 5 | configEmoji: [ 6 | ['recommended', '✅'], 7 | ['flat/recommended', '✅'], 8 | ], 9 | postprocess: (doc) => { 10 | return doc.replace(/✅\s*✅/gu, '✅') 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | test('can require index file', () => { 4 | expect(require('../index')).toBeInstanceOf(Object) 5 | }) 6 | 7 | test('rule set', () => { 8 | const plugin = require('../index') 9 | expect(plugin.configs.recommended.rules).toEqual( 10 | plugin.configs['flat/recommended'].rules, 11 | ) 12 | expect(plugin.configs['flat/recommended'].plugins.promise).toBe(plugin) 13 | }) 14 | -------------------------------------------------------------------------------- /.github/workflows/compliance.yml: -------------------------------------------------------------------------------- 1 | name: Compliance 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, edited, reopened] 6 | 7 | permissions: 8 | pull-requests: write 9 | 10 | jobs: 11 | compliance: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: mtfoley/pr-compliance-action@11b664f0fcf2c4ce954f05ccfcaab6e52b529f86 15 | with: 16 | body-auto-close: false 17 | body-regex: '.*' 18 | ignore-team-members: false 19 | -------------------------------------------------------------------------------- /rules/lib/is-named-callback.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let callbacks = ['done', 'cb', 'callback', 'next'] 4 | 5 | module.exports = function isNamedCallback(potentialCallbackName, exceptions) { 6 | for (let i = 0; i < exceptions.length; i++) { 7 | callbacks = callbacks.filter((item) => { 8 | return item !== exceptions[i] 9 | }) 10 | } 11 | return callbacks.some((trueCallbackName) => { 12 | return potentialCallbackName === trueCallbackName 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /rules/lib/is-callback.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const isNamedCallback = require('./is-named-callback') 4 | 5 | function isCallback(node, exceptions) { 6 | const isCallExpression = node.type === 'CallExpression' 7 | // istanbul ignore next -- always invoked on `CallExpression` 8 | const callee = node.callee || {} 9 | const nameIsCallback = isNamedCallback(callee.name, exceptions) 10 | const isCB = isCallExpression && nameIsCallback 11 | return isCB 12 | } 13 | 14 | module.exports = isCallback 15 | -------------------------------------------------------------------------------- /rules/lib/is-inside-promise.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function isInsidePromise(node) { 4 | const isFunctionExpression = 5 | node.type === 'FunctionExpression' || 6 | node.type === 'ArrowFunctionExpression' 7 | const parent = node.parent || {} 8 | const callee = parent.callee || {} 9 | const name = (callee.property && callee.property.name) || '' 10 | const parentIsPromise = name === 'then' || name === 'catch' 11 | const isInCB = isFunctionExpression && parentIsPromise 12 | return isInCB 13 | } 14 | 15 | module.exports = isInsidePromise 16 | -------------------------------------------------------------------------------- /docs/rules/spec-only.md: -------------------------------------------------------------------------------- 1 | # Disallow use of non-standard Promise static methods (`promise/spec-only`) 2 | 3 | 4 | 5 | It may become difficult to migrate code depending on non-standard Promise 6 | extensions. This rule reports any such method usage. 7 | 8 | ## Valid 9 | 10 | ```js 11 | const x = Promise.resolve('good') 12 | ``` 13 | 14 | ## Invalid 15 | 16 | ```js 17 | const x = Promise.done('bad') 18 | ``` 19 | 20 | ## Options 21 | 22 | ### `allowedMethods` 23 | 24 | An array of allowed non-standard methods. Defaults to an empty array. 25 | -------------------------------------------------------------------------------- /rules/lib/get-docs-url.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const REPO_URL = 'https://github.com/eslint-community/eslint-plugin-promise' 4 | 5 | /** 6 | * Generates the URL to documentation for the given rule name. It uses the 7 | * package version to build the link to a tagged version of the 8 | * documentation file. 9 | * 10 | * @param {string} ruleName - Name of the eslint rule 11 | * @returns {string} URL to the documentation for the given rule 12 | */ 13 | function getDocsUrl(ruleName) { 14 | return `${REPO_URL}/blob/main/docs/rules/${ruleName}.md` 15 | } 16 | 17 | module.exports = getDocsUrl 18 | -------------------------------------------------------------------------------- /rules/lib/is-member-call-with-object-name.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * @param {string} objectName 5 | * @param {Node} node 6 | * @returns {node is CallExpression} 7 | */ 8 | function isMemberCallWithObjectName(objectName, node) { 9 | return ( 10 | node.type === 'CallExpression' && 11 | node.callee.type === 'MemberExpression' && 12 | ((node.callee.object.type === 'Identifier' && 13 | node.callee.object.name === objectName) || 14 | isMemberCallWithObjectName(objectName, node.callee.object)) 15 | ) 16 | } 17 | 18 | module.exports = isMemberCallWithObjectName 19 | -------------------------------------------------------------------------------- /docs/rules/no-return-in-finally.md: -------------------------------------------------------------------------------- 1 | # Disallow return statements in `finally()` (`promise/no-return-in-finally`) 2 | 3 | ⚠️ This rule _warns_ in the following configs: ✅ `flat/recommended`, ✅ 4 | `recommended`. 5 | 6 | 7 | 8 | Disallow return statements inside a callback passed to `finally()`, since 9 | nothing would consume what's returned. 10 | 11 | #### Valid 12 | 13 | ```js 14 | myPromise.finally(function (val) { 15 | console.log('value:', val) 16 | }) 17 | ``` 18 | 19 | #### Invalid 20 | 21 | ```js 22 | myPromise.finally(function (val) { 23 | return val 24 | }) 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/rules/no-nesting.md: -------------------------------------------------------------------------------- 1 | # Disallow nested `then()` or `catch()` statements (`promise/no-nesting`) 2 | 3 | ⚠️ This rule _warns_ in the following configs: ✅ `flat/recommended`, ✅ 4 | `recommended`. 5 | 6 | 7 | 8 | #### Valid 9 | 10 | ```js 11 | myPromise.then(doSomething).then(doSomethingElse).catch(errors) 12 | ``` 13 | 14 | #### Invalid 15 | 16 | ```js 17 | myPromise.then((val) => doSomething(val).then(doSomethingElse)) 18 | 19 | myPromise.then((val) => doSomething(val).catch(errors)) 20 | 21 | myPromise.catch((err) => doSomething(err).then(doSomethingElse)) 22 | 23 | myPromise.catch((err) => doSomething(err).catch(errors)) 24 | ``` 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | [Description of the issue or feature request] 4 | 5 | ### Steps to Reproduce 6 | 7 | 1. [First Step] 8 | 2. [Second Step] 9 | 3. [and so on...] 10 | 11 | **Expected behavior:** [What you expect to happen] 12 | 13 | **Actual behavior:** [What actually happens] 14 | 15 | ### Versions 16 | 17 | [Please fill this in if you are submitting a bug report] 18 | 19 | - Node version: [Replace this] 20 | - ESLint version: [Replace this] 21 | - eslint-plugin-promise version: [Replace this] 22 | 23 | ### Additional Information 24 | 25 | [Any additional information, configuration or data that might be necessary to 26 | reproduce the issue] 27 | -------------------------------------------------------------------------------- /docs/rules/no-native.md: -------------------------------------------------------------------------------- 1 | # Require creating a `Promise` constructor before using it in an ES5 environment (`promise/no-native`) 2 | 3 | 🚫 This rule is _disabled_ in the following configs: ✅ `flat/recommended`, ✅ 4 | `recommended`. 5 | 6 | 7 | 8 | Ensure that `Promise` is included fresh in each file instead of relying on the 9 | existence of a native promise implementation. Helpful if you want to use 10 | `bluebird` or if you don't intend to use an ES6 Promise shim. 11 | 12 | #### Valid 13 | 14 | ```js 15 | const Promise = require('bluebird') 16 | const x = Promise.resolve('good') 17 | ``` 18 | 19 | #### Invalid 20 | 21 | ```js 22 | const x = Promise.resolve('bad') 23 | ``` 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Jamund Ferguson 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /docs/rules/no-multiple-resolved.md: -------------------------------------------------------------------------------- 1 | # Disallow creating new promises with paths that resolve multiple times (`promise/no-multiple-resolved`) 2 | 3 | 4 | 5 | This rule warns of paths that resolve multiple times in executor functions that 6 | Promise constructors. 7 | 8 | #### Valid 9 | 10 | ```js 11 | new Promise((resolve, reject) => { 12 | fn((error, value) => { 13 | if (error) { 14 | reject(error) 15 | } else { 16 | resolve(value) 17 | } 18 | }) 19 | }) 20 | ``` 21 | 22 | #### Invalid 23 | 24 | ```js 25 | new Promise((resolve, reject) => { 26 | fn((error, value) => { 27 | if (error) { 28 | reject(error) 29 | } 30 | 31 | resolve(value) // Both `reject` and `resolve` may be called. 32 | }) 33 | }) 34 | ``` 35 | -------------------------------------------------------------------------------- /rules/lib/eslint-compat.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function getSourceCode(context) { 4 | if (context.sourceCode != null) { 5 | return context.sourceCode 6 | } 7 | 8 | return context.getSourceCode() 9 | } 10 | 11 | function getAncestors(context, node) { 12 | const sourceCode = getSourceCode(context) 13 | if (typeof sourceCode.getAncestors === 'function') { 14 | return sourceCode.getAncestors(node) 15 | } 16 | 17 | return context.getAncestors(node) 18 | } 19 | 20 | function getScope(context, node) { 21 | const sourceCode = getSourceCode(context) 22 | if (typeof sourceCode.getScope === 'function') { 23 | return sourceCode.getScope(node) 24 | } 25 | 26 | return context.getScope(node) 27 | } 28 | 29 | module.exports = { 30 | getSourceCode, 31 | getAncestors, 32 | getScope, 33 | } 34 | -------------------------------------------------------------------------------- /rules/avoid-new.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rule: avoid-new 3 | * Avoid creating new promises outside of utility libraries. 4 | */ 5 | 6 | 'use strict' 7 | 8 | const getDocsUrl = require('./lib/get-docs-url') 9 | 10 | module.exports = { 11 | meta: { 12 | type: 'suggestion', 13 | docs: { 14 | description: 15 | 'Disallow creating `new` promises outside of utility libs (use [util.promisify][] instead).', 16 | url: getDocsUrl('avoid-new'), 17 | }, 18 | schema: [], 19 | messages: { 20 | avoidNew: 'Avoid creating new promises.', 21 | }, 22 | }, 23 | create(context) { 24 | return { 25 | NewExpression(node) { 26 | if (node.callee.name === 'Promise') { 27 | context.report({ node, messageId: 'avoidNew' }) 28 | } 29 | }, 30 | } 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2024, 4 | "sourceType": "script" 5 | }, 6 | "plugins": ["eslint-plugin", "n", "prettier"], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:eslint-plugin/recommended", 10 | "plugin:n/recommended", 11 | "plugin:prettier/recommended" 12 | ], 13 | "rules": { 14 | "no-var": "error", 15 | "object-shorthand": "error", 16 | "prefer-arrow-callback": "error", 17 | "prefer-const": "error", 18 | "strict": ["error", "global"], 19 | "eslint-plugin/require-meta-docs-description": [ 20 | "error", 21 | { "pattern": "^(Enforce|Require|Disallow|Prefer).+\\.$" } 22 | ], 23 | "eslint-plugin/prefer-placeholders": "error", 24 | "eslint-plugin/test-case-shorthand-strings": "error", 25 | "prettier/prettier": "error" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rules/lib/is-inside-callback.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const isInsidePromise = require('./is-inside-promise') 4 | 5 | /** 6 | * @param {import('eslint').Rule.Node} node 7 | * @param {boolean} [exemptDeclarations] 8 | */ 9 | function isInsideCallback(node, exemptDeclarations) { 10 | const isFunction = 11 | node.type === 'FunctionExpression' || 12 | node.type === 'ArrowFunctionExpression' || 13 | (!exemptDeclarations && node.type === 'FunctionDeclaration') // this may be controversial 14 | 15 | // it's totally fine to use promises inside promises 16 | if (isInsidePromise(node)) return 17 | 18 | const name = node.params && node.params[0] && node.params[0].name 19 | const firstArgIsError = name === 'err' || name === 'error' 20 | const isInACallback = isFunction && firstArgIsError 21 | return isInACallback 22 | } 23 | 24 | module.exports = isInsideCallback 25 | -------------------------------------------------------------------------------- /__tests__/avoid-new.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/avoid-new') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 6, 8 | }, 9 | }) 10 | 11 | const errorMessage = 'Avoid creating new promises.' 12 | 13 | ruleTester.run('avoid-new', rule, { 14 | valid: [ 15 | 'Promise.resolve()', 16 | 'Promise.reject()', 17 | 'Promise.all()', 18 | 'new Horse()', 19 | 'new PromiseLikeThing()', 20 | 'new Promise.resolve()', 21 | ], 22 | 23 | invalid: [ 24 | { 25 | code: 'var x = new Promise(function (x, y) {})', 26 | errors: [{ message: errorMessage }], 27 | }, 28 | { 29 | code: 'new Promise()', 30 | errors: [{ message: errorMessage }], 31 | }, 32 | { 33 | code: 'Thing(new Promise(() => {}))', 34 | errors: [{ message: errorMessage }], 35 | }, 36 | ], 37 | }) 38 | -------------------------------------------------------------------------------- /.github/release-please/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/v16.12.0/schemas/config.json", 3 | "release-type": "node", 4 | "include-component-in-tag": false, 5 | "changelog-sections": [ 6 | { "type": "feat", "section": "🌟 Features", "hidden": false }, 7 | { "type": "fix", "section": "🩹 Fixes", "hidden": false }, 8 | { "type": "docs", "section": "📚 Documentation", "hidden": false }, 9 | 10 | { "type": "chore", "section": "🧹 Chores", "hidden": false }, 11 | { "type": "perf", "section": "🧹 Chores", "hidden": false }, 12 | { "type": "refactor", "section": "🧹 Chores", "hidden": false }, 13 | { "type": "test", "section": "🧹 Chores", "hidden": false }, 14 | 15 | { "type": "build", "section": "🤖 Automation", "hidden": false }, 16 | { "type": "ci", "section": "🤖 Automation", "hidden": true } 17 | ], 18 | "packages": { 19 | ".": {} 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/rules/prefer-catch.md: -------------------------------------------------------------------------------- 1 | # Prefer `catch` to `then(a, b)`/`then(null, b)` for handling errors (`promise/prefer-catch`) 2 | 3 | 🔧 This rule is automatically fixable by the 4 | [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). 5 | 6 | 7 | 8 | A `then` call with two arguments can make it more difficult to recognize that a 9 | catch error handler is present and can be less clear as to the order in which 10 | errors will be handled. 11 | 12 | ## Rule Details 13 | 14 | The second argument of a `then` call may be thought to handle any errors in the 15 | first argument, but it will only handle errors earlier in the Promise chain. 16 | 17 | Examples of **incorrect** code for this rule: 18 | 19 | ```js 20 | prom.then(fn1).then(fn2) 21 | prom.catch(handleErr).then(handle) 22 | ``` 23 | 24 | Examples of **incorrect** code for this rule: 25 | 26 | ```js 27 | hey.then(fn1, fn2) 28 | hey.then(null, fn2) 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/rules/no-return-wrap.md: -------------------------------------------------------------------------------- 1 | # Disallow wrapping values in `Promise.resolve` or `Promise.reject` when not needed (`promise/no-return-wrap`) 2 | 3 | 💼 This rule is enabled in the following configs: ✅ `flat/recommended`, ✅ 4 | `recommended`. 5 | 6 | 7 | 8 | Ensure that inside a `then()` or a `catch()` we always `return` or `throw` a raw 9 | value instead of wrapping in `Promise.resolve` or `Promise.reject` 10 | 11 | #### Valid 12 | 13 | ```js 14 | myPromise.then(function (val) { 15 | return val * 2 16 | }) 17 | myPromise.then(function (val) { 18 | throw 'bad thing' 19 | }) 20 | ``` 21 | 22 | #### Invalid 23 | 24 | ```js 25 | myPromise.then(function (val) { 26 | return Promise.resolve(val * 2) 27 | }) 28 | myPromise.then(function (val) { 29 | return Promise.reject('bad thing') 30 | }) 31 | ``` 32 | 33 | #### Options 34 | 35 | ##### `allowReject` 36 | 37 | Pass `{ allowReject: true }` as an option to this rule to permit wrapping 38 | returned values with `Promise.reject`, such as when you would use it as another 39 | way to reject the promise. 40 | -------------------------------------------------------------------------------- /docs/rules/no-promise-in-callback.md: -------------------------------------------------------------------------------- 1 | # Disallow using promises inside of callbacks (`promise/no-promise-in-callback`) 2 | 3 | ⚠️ This rule _warns_ in the following configs: ✅ `flat/recommended`, ✅ 4 | `recommended`. 5 | 6 | 7 | 8 | Discourages the use of promises inside callbacks. 9 | 10 | ## Rule Details 11 | 12 | Promises and callbacks are different ways to handle asynchronous code and should 13 | not be mixed. 14 | 15 | Examples of **incorrect** code for this rule: 16 | 17 | ```js 18 | doSomething((err, val) => { 19 | if (err) console.error(err) 20 | else doSomethingElse(val).then(console.log) 21 | }) 22 | ``` 23 | 24 | Examples of **correct** code for this rule: 25 | 26 | ```js 27 | promisify(doSomething)() 28 | .then(doSomethingElse) 29 | .then(console.log) 30 | .catch(console.error) 31 | ``` 32 | 33 | ## Options 34 | 35 | ### `exemptDeclarations` 36 | 37 | Whether or not to exempt function declarations. Defaults to `false`. 38 | 39 | ## When Not To Use It 40 | 41 | If you do not want to be notified when using promises inside of callbacks, you 42 | can safely disable this rule. 43 | -------------------------------------------------------------------------------- /docs/rules/prefer-await-to-callbacks.md: -------------------------------------------------------------------------------- 1 | # Prefer `async`/`await` to the callback pattern (`promise/prefer-await-to-callbacks`) 2 | 3 | 4 | 5 | `async`/`await` is a clearer pattern to follow than using callbacks. 6 | 7 | ## Rule details 8 | 9 | ES2017's `async`/`await` makes it easier to deal with asynchronous code than the 10 | callback pattern. 11 | 12 | Examples of **incorrect** code for this rule: 13 | 14 | ```js 15 | cb() 16 | callback() 17 | doSomething(arg, (err) => {}) 18 | function doSomethingElse(cb) {} 19 | ``` 20 | 21 | Examples of **correct** code for this rule: 22 | 23 | ```js 24 | await doSomething(arg) 25 | async function doSomethingElse() {} 26 | yield yieldValue(err => {}) 27 | eventEmitter.on('error', err => {}) 28 | ``` 29 | 30 | ## When Not To Use It 31 | 32 | If you are not targeting an ES2017 or higher environment and cannot transpile 33 | `async`/`await`, you should disable this rule. 34 | 35 | ## Further Reading 36 | 37 | - [Making asynchronous programming easier with async and await on MDN](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Promises) 38 | -------------------------------------------------------------------------------- /__tests__/no-return-in-finally.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { RuleTester } = require('./rule-tester') 4 | const rule = require('../rules/no-return-in-finally') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 6, 8 | }, 9 | }) 10 | 11 | const message = 'No return in finally' 12 | 13 | ruleTester.run('no-return-in-finally', rule, { 14 | valid: [ 15 | 'Promise.resolve(1).finally(() => { console.log(2) })', 16 | 'Promise.reject(4).finally(() => { console.log(2) })', 17 | 'Promise.reject(4).finally(() => {})', 18 | 'myPromise.finally(() => {});', 19 | 'Promise.resolve(1).finally(function () { })', 20 | ], 21 | invalid: [ 22 | { 23 | code: 'Promise.resolve(1).finally(() => { return 2 })', 24 | errors: [{ message }], 25 | }, 26 | { 27 | code: 'Promise.reject(0).finally(() => { return 2 })', 28 | errors: [{ message }], 29 | }, 30 | { 31 | code: 'myPromise.finally(() => { return 2 });', 32 | errors: [{ message }], 33 | }, 34 | { 35 | code: 'Promise.resolve(1).finally(function () { return 2 })', 36 | errors: [{ message }], 37 | }, 38 | ], 39 | }) 40 | -------------------------------------------------------------------------------- /docs/rules/no-new-statics.md: -------------------------------------------------------------------------------- 1 | # Disallow calling `new` on a Promise static method (`promise/no-new-statics`) 2 | 3 | 💼 This rule is enabled in the following configs: ✅ `flat/recommended`, ✅ 4 | `recommended`. 5 | 6 | 🔧 This rule is automatically fixable by the 7 | [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). 8 | 9 | 10 | 11 | Calling a Promise static method with `new` is invalid, resulting in a 12 | `TypeError` at runtime. 13 | 14 | ## Rule Details 15 | 16 | This rule is aimed at flagging instances where a Promise static method is called 17 | with `new`. 18 | 19 | Examples for **incorrect** code for this rule: 20 | 21 | ```js 22 | new Promise.resolve(value) 23 | new Promise.reject(error) 24 | new Promise.race([p1, p2]) 25 | new Promise.all([p1, p2]) 26 | ``` 27 | 28 | Examples for **correct** code for this rule: 29 | 30 | ```js 31 | Promise.resolve(value) 32 | Promise.reject(error) 33 | Promise.race([p1, p2]) 34 | Promise.all([p1, p2]) 35 | ``` 36 | 37 | ## When Not To Use It 38 | 39 | If you do not want to be notified when calling `new` on a Promise static method, 40 | you can safely disable this rule. 41 | -------------------------------------------------------------------------------- /rules/no-new-statics.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PROMISE_STATICS = require('./lib/promise-statics') 4 | const getDocsUrl = require('./lib/get-docs-url') 5 | 6 | module.exports = { 7 | meta: { 8 | type: 'problem', 9 | docs: { 10 | description: 'Disallow calling `new` on a Promise static method.', 11 | url: getDocsUrl('no-new-statics'), 12 | }, 13 | fixable: 'code', 14 | schema: [], 15 | messages: { 16 | avoidNewStatic: "Avoid calling 'new' on 'Promise.{{ name }}()'", 17 | }, 18 | }, 19 | create(context) { 20 | return { 21 | NewExpression(node) { 22 | if ( 23 | node.callee.type === 'MemberExpression' && 24 | node.callee.object.name === 'Promise' && 25 | PROMISE_STATICS.has(node.callee.property.name) 26 | ) { 27 | context.report({ 28 | node, 29 | messageId: 'avoidNewStatic', 30 | data: { name: node.callee.property.name }, 31 | fix(fixer) { 32 | return fixer.replaceTextRange( 33 | [node.range[0], node.range[0] + 'new '.length], 34 | '', 35 | ) 36 | }, 37 | }) 38 | } 39 | }, 40 | } 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Release Please 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | release_please: 13 | name: release-please 14 | runs-on: ubuntu-latest 15 | outputs: 16 | releaseCreated: ${{ steps.release.outputs.release_created }} 17 | permissions: 18 | contents: write 19 | pull-requests: write 20 | steps: 21 | - uses: googleapis/release-please-action@v4 22 | id: release 23 | with: 24 | config-file: .github/release-please/config.json 25 | manifest-file: .github/release-please/manifest.json 26 | 27 | npm_publish: 28 | name: Publish to npm 29 | runs-on: ubuntu-latest 30 | needs: release_please 31 | if: needs.release_please.outputs.releaseCreated 32 | permissions: 33 | id-token: write 34 | steps: 35 | - uses: actions/checkout@v4 36 | with: 37 | show-progress: false 38 | - uses: actions/setup-node@v4 39 | with: 40 | node-version: lts/* 41 | registry-url: 'https://registry.npmjs.org' 42 | - run: npm i npm@latest -g 43 | - run: npm ci 44 | - run: npm publish --provenance --access public 45 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: 👔 Format 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | permissions: 13 | contents: write 14 | 15 | jobs: 16 | format: 17 | if: github.repository == 'eslint-community/eslint-plugin-promise' 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: ⬇️ Checkout repo 22 | uses: actions/checkout@v4 23 | 24 | - name: ⎔ Setup node 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: lts/* 28 | 29 | - name: 📥 Install deps 30 | run: npm ci 31 | 32 | - name: 👔 Format 33 | run: npm run format 34 | 35 | - name: 💪 Commit 36 | run: | 37 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 38 | git config --local user.name "github-actions[bot]" 39 | 40 | git add . 41 | if [ -z "$(git status --porcelain)" ]; then 42 | echo "💿 no formatting changed" 43 | exit 0 44 | fi 45 | git commit -m "chore: format" 46 | git push 47 | echo "💿 pushed formatting changes https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)" 48 | -------------------------------------------------------------------------------- /rules/lib/has-promise-callback.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Library: Has Promise Callback 3 | * Makes sure that an Expression node is part of a promise 4 | * with callback functions (like then() or catch()) 5 | */ 6 | 7 | 'use strict' 8 | 9 | /** 10 | * @typedef {import('estree').SimpleCallExpression} CallExpression 11 | * @typedef {import('estree').MemberExpression} MemberExpression 12 | * @typedef {import('estree').Identifier} Identifier 13 | * 14 | * @typedef {object} NameIsThenOrCatch 15 | * @property {'then' | 'catch'} name 16 | * 17 | * @typedef {object} PropertyIsThenOrCatch 18 | * @property {Identifier & NameIsThenOrCatch} property 19 | * 20 | * @typedef {object} CalleeIsPromiseCallback 21 | * @property {MemberExpression & PropertyIsThenOrCatch} callee 22 | * 23 | * @typedef {CallExpression & CalleeIsPromiseCallback} HasPromiseCallback 24 | */ 25 | /** 26 | * @param {import('estree').Node} node 27 | * @returns {node is HasPromiseCallback} 28 | */ 29 | function hasPromiseCallback(node) { 30 | // istanbul ignore if -- only being called within `CallExpression` 31 | if (node.type !== 'CallExpression') return 32 | if (node.callee.type !== 'MemberExpression') return 33 | const propertyName = node.callee.property.name 34 | return propertyName === 'then' || propertyName === 'catch' 35 | } 36 | 37 | module.exports = hasPromiseCallback 38 | -------------------------------------------------------------------------------- /rules/lib/parentheses.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from `eslint-plugin-unicorn` 3 | * @license MIT 4 | */ 5 | 6 | 'use strict' 7 | const { 8 | isParenthesized, 9 | isOpeningParenToken, 10 | isClosingParenToken, 11 | } = require('@eslint-community/eslint-utils') 12 | 13 | /* 14 | Get how many times the node is parenthesized. 15 | 16 | @param {Node} node - The node to be checked. 17 | @param {SourceCode} sourceCode - The source code object. 18 | @returns {number} 19 | */ 20 | function getParenthesizedTimes(node, sourceCode) { 21 | let times = 0 22 | 23 | while (isParenthesized(times + 1, node, sourceCode)) { 24 | times++ 25 | } 26 | 27 | return times 28 | } 29 | 30 | /* 31 | Get all parentheses tokens around the node. 32 | 33 | @param {Node} node - The node to be checked. 34 | @param {SourceCode} sourceCode - The source code object. 35 | @returns {Token[]} 36 | */ 37 | function getParentheses(node, sourceCode) { 38 | const count = getParenthesizedTimes(node, sourceCode) 39 | 40 | if (count === 0) { 41 | return [] 42 | } 43 | 44 | return [ 45 | ...sourceCode.getTokensBefore(node, { count, filter: isOpeningParenToken }), 46 | ...sourceCode.getTokensAfter(node, { count, filter: isClosingParenToken }), 47 | ] 48 | } 49 | 50 | module.exports = { 51 | isParenthesized, 52 | getParenthesizedTimes, 53 | getParentheses, 54 | } 55 | -------------------------------------------------------------------------------- /docs/rules/prefer-await-to-then.md: -------------------------------------------------------------------------------- 1 | # Prefer `await` to `then()`/`catch()`/`finally()` for reading Promise values (`promise/prefer-await-to-then`) 2 | 3 | 4 | 5 | #### Valid 6 | 7 | ```js 8 | async function example() { 9 | let val = await myPromise() 10 | val = doSomethingSync(val) 11 | return doSomethingElseAsync(val) 12 | } 13 | 14 | async function exampleTwo() { 15 | try { 16 | let val = await myPromise() 17 | val = doSomethingSync(val) 18 | return await doSomethingElseAsync(val) 19 | } catch (err) { 20 | errors(err) 21 | } 22 | } 23 | ``` 24 | 25 | #### Invalid 26 | 27 | ```js 28 | function example() { 29 | return myPromise.then(doSomethingSync).then(doSomethingElseAsync) 30 | } 31 | 32 | function exampleTwo() { 33 | return myPromise 34 | .then(doSomethingSync) 35 | .then(doSomethingElseAsync) 36 | .catch(errors) 37 | } 38 | 39 | function exampleThree() { 40 | return myPromise.catch(errors) 41 | } 42 | 43 | function exampleFour() { 44 | return myPromise.finally(cleanup) 45 | } 46 | ``` 47 | 48 | ## Options 49 | 50 | ### `strict` 51 | 52 | Normally, this rule allows `then` or `catch` following an `await` (or `yield`) 53 | expression. Setting this option to `true` will err on such cases: 54 | 55 | This will fail with the `strict` option: 56 | 57 | ```js 58 | async function hi() { 59 | await thing().then() 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /rules/lib/is-promise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Library: isPromise 3 | * Makes sure that an Expression node is part of a promise. 4 | */ 5 | 'use strict' 6 | 7 | const PROMISE_STATICS = require('./promise-statics') 8 | 9 | function isPromise(expression) { 10 | return ( 11 | // hello.then() 12 | (expression.type === 'CallExpression' && 13 | expression.callee.type === 'MemberExpression' && 14 | expression.callee.property.name === 'then') || 15 | // hello.catch() 16 | (expression.type === 'CallExpression' && 17 | expression.callee.type === 'MemberExpression' && 18 | expression.callee.property.name === 'catch') || 19 | // hello.finally() 20 | (expression.type === 'CallExpression' && 21 | expression.callee.type === 'MemberExpression' && 22 | expression.callee.property.name === 'finally') || 23 | // somePromise.ANYTHING() 24 | (expression.type === 'CallExpression' && 25 | expression.callee.type === 'MemberExpression' && 26 | isPromise(expression.callee.object)) || 27 | // Promise.STATIC_METHOD() 28 | (expression.type === 'CallExpression' && 29 | expression.callee.type === 'MemberExpression' && 30 | expression.callee.object.type === 'Identifier' && 31 | expression.callee.object.name === 'Promise' && 32 | PROMISE_STATICS.has(expression.callee.property.name) && 33 | expression.callee.property.name !== 'withResolvers') 34 | ) 35 | } 36 | 37 | module.exports = isPromise 38 | -------------------------------------------------------------------------------- /rules/no-return-in-finally.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const getDocsUrl = require('./lib/get-docs-url') 4 | const isPromise = require('./lib/is-promise') 5 | 6 | module.exports = { 7 | meta: { 8 | type: 'problem', 9 | docs: { 10 | description: 'Disallow return statements in `finally()`.', 11 | url: getDocsUrl('no-return-in-finally'), 12 | }, 13 | schema: [], 14 | messages: { 15 | avoidReturnInFinally: 'No return in finally', 16 | }, 17 | }, 18 | create(context) { 19 | return { 20 | CallExpression(node) { 21 | if (isPromise(node)) { 22 | if ( 23 | node.callee && 24 | node.callee.property && 25 | node.callee.property.name === 'finally' 26 | ) { 27 | // istanbul ignore else -- passing `isPromise` means should have a body 28 | if ( 29 | node.arguments && 30 | node.arguments[0] && 31 | node.arguments[0].body && 32 | node.arguments[0].body.body 33 | ) { 34 | if ( 35 | node.arguments[0].body.body.some((statement) => { 36 | return statement.type === 'ReturnStatement' 37 | }) 38 | ) { 39 | context.report({ 40 | node: node.callee.property, 41 | messageId: 'avoidReturnInFinally', 42 | }) 43 | } 44 | } 45 | } 46 | } 47 | }, 48 | } 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /rules/lib/is-promise-constructor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Library: isPromiseConstructor 3 | * Makes sure that an Expression node is new Promise(). 4 | */ 5 | 'use strict' 6 | 7 | /** 8 | * @typedef {import('estree').Node} Node 9 | * @typedef {import('estree').Expression} Expression 10 | * @typedef {import('estree').NewExpression} NewExpression 11 | * @typedef {import('estree').FunctionExpression} FunctionExpression 12 | * @typedef {import('estree').ArrowFunctionExpression} ArrowFunctionExpression 13 | * 14 | * @typedef {NewExpression & { callee: { type: 'Identifier', name: 'Promise' } }} NewPromise 15 | * @typedef {NewPromise & { arguments: [FunctionExpression | ArrowFunctionExpression] }} NewPromiseWithInlineExecutor 16 | * 17 | */ 18 | /** 19 | * Checks whether the given node is new Promise(). 20 | * @param {Node} node 21 | * @returns {node is NewPromise} 22 | */ 23 | function isPromiseConstructor(node) { 24 | return ( 25 | node.type === 'NewExpression' && 26 | node.callee.type === 'Identifier' && 27 | node.callee.name === 'Promise' 28 | ) 29 | } 30 | 31 | /** 32 | * Checks whether the given node is new Promise(() => {}). 33 | * @param {Node} node 34 | * @returns {node is NewPromiseWithInlineExecutor} 35 | */ 36 | function isPromiseConstructorWithInlineExecutor(node) { 37 | return ( 38 | isPromiseConstructor(node) && 39 | node.arguments.length === 1 && 40 | (node.arguments[0].type === 'FunctionExpression' || 41 | node.arguments[0].type === 'ArrowFunctionExpression') 42 | ) 43 | } 44 | 45 | module.exports = { 46 | isPromiseConstructor, 47 | isPromiseConstructorWithInlineExecutor, 48 | } 49 | -------------------------------------------------------------------------------- /rules/fix/remove-argument.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from `eslint-plugin-unicorn` 3 | * @license MIT 4 | */ 5 | 6 | 'use strict' 7 | const { isCommaToken } = require('@eslint-community/eslint-utils') 8 | const { getParentheses } = require('../lib/parentheses.js') 9 | 10 | function removeArgument(fixer, node, sourceCode) { 11 | const callExpression = node.parent 12 | const index = callExpression.arguments.indexOf(node) 13 | const parentheses = getParentheses(node, sourceCode) 14 | const firstToken = parentheses[0] || node 15 | const lastToken = parentheses.at(-1) || node 16 | 17 | let [start] = firstToken.range 18 | let [, end] = lastToken.range 19 | 20 | if (index !== 0) { 21 | start = sourceCode.getTokenBefore(firstToken).range[0] 22 | } 23 | 24 | // If there are subsequent arguments, the trailing comma must be removed too 25 | /* istanbul ignore else */ 26 | if (index < callExpression.arguments.length - 1) { 27 | let tokenAfter = sourceCode.getTokenAfter(lastToken) 28 | /* istanbul ignore else */ 29 | if (isCommaToken(tokenAfter)) { 30 | // Advance to start of next token (after whitespace) 31 | tokenAfter = sourceCode.getTokenAfter(tokenAfter) 32 | end = tokenAfter.range[0] 33 | } 34 | } 35 | // If the removed argument is the only argument, the trailing comma must be removed too 36 | else if (callExpression.arguments.length === 1) { 37 | const tokenAfter = sourceCode.getTokenBefore(lastToken) 38 | if (isCommaToken(tokenAfter)) { 39 | end = tokenAfter[1] 40 | } 41 | } 42 | 43 | return fixer.replaceTextRange([start, end], '') 44 | } 45 | 46 | module.exports = removeArgument 47 | -------------------------------------------------------------------------------- /docs/rules/avoid-new.md: -------------------------------------------------------------------------------- 1 | # Disallow creating `new` promises outside of utility libs (use [util.promisify][] instead) (`promise/avoid-new`) 2 | 3 | 🚫 This rule is _disabled_ in the following configs: ✅ `flat/recommended`, ✅ 4 | `recommended`. 5 | 6 | 7 | 8 | Avoid using `new Promise` in favour of utility libraries or 9 | `Promise.resolve`/`reject`. 10 | 11 | ## Rule Details 12 | 13 | Creating promises using `new Promise` can be used to promisify Node-style 14 | callbacks. However, you can use Node's [`util.promisify`]() instead. 15 | 16 | `new Promise` is also sometimes misused to wrap a value or error into a promise. 17 | However, this can be done more concisely and clearly with `Promise.resolve` and 18 | `Promise.reject`. 19 | 20 | Examples of **incorrect** code for this rule: 21 | 22 | ```js 23 | function promisifiedFn(arg) { 24 | return new Promise((resolve, reject) => { 25 | callbackStyleFn(arg, (error, result) => 26 | error ? reject(error) : resolve(result), 27 | ) 28 | }) 29 | } 30 | 31 | new Promise((resolve, reject) => resolve(1)) 32 | new Promise((resolve, reject) => reject(new Error('oops'))) 33 | ``` 34 | 35 | Examples of **correct** code for this rule: 36 | 37 | ```js 38 | import util from 'util' 39 | const promisifiedFn = util.promisify(callbackStyleFn) 40 | 41 | Promise.resolve(1) 42 | Promise.reject(new Error('oops')) 43 | ``` 44 | 45 | ## When Not To Use It 46 | 47 | If you are creating a utility library without [util.promisify]() or do not want 48 | to be notified when using `new Promise`, you can safely disable this rule. 49 | 50 | [util.promisify]: https://nodejs.org/api/util.html#util_util_promisify_original 51 | -------------------------------------------------------------------------------- /docs/rules/param-names.md: -------------------------------------------------------------------------------- 1 | # Enforce consistent param names and ordering when creating new promises (`promise/param-names`) 2 | 3 | 💼 This rule is enabled in the following configs: ✅ `flat/recommended`, ✅ 4 | `recommended`. 5 | 6 | 7 | 8 | Enforce standard parameter names for Promise constructors 9 | 10 | #### Valid 11 | 12 | ```js 13 | new Promise(function (resolve) { ... }) 14 | new Promise(function (resolve, reject) { ... }) 15 | new Promise(function (_resolve, _reject) { ... }) // Unused marker for parameters are allowed 16 | ``` 17 | 18 | #### Invalid 19 | 20 | ```js 21 | new Promise(function (reject, resolve) { ... }) // incorrect order 22 | new Promise(function (ok, fail) { ... }) // non-standard parameter names 23 | new Promise(function (_, reject) { ... }) // a simple underscore is not allowed 24 | ``` 25 | 26 | Ensures that `new Promise()` is instantiated with the parameter names 27 | `resolve, reject` to avoid confusion with order such as `reject, resolve`. The 28 | Promise constructor uses the 29 | [RevealingConstructor pattern](https://blog.domenic.me/the-revealing-constructor-pattern/). 30 | Using the same parameter names as the language specification makes code more 31 | uniform and easier to understand. 32 | 33 | #### Options 34 | 35 | ##### `resolvePattern` 36 | 37 | You can pass a `{ resolvePattern: "^_?resolve$" }` as an option to this rule to 38 | the first argument name pattern that the rule allows. Default is 39 | `"^_?resolve$"`. 40 | 41 | ##### `rejectPattern` 42 | 43 | You can pass a `{ rejectPattern: "^_?reject$" }` as an option to this rule to 44 | the second argument name pattern that the rule allows. Default is 45 | `"^_?reject$"`. 46 | -------------------------------------------------------------------------------- /__tests__/prefer-catch.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/prefer-catch') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 8, 8 | }, 9 | }) 10 | 11 | const message = 'Prefer `catch` to `then(a, b)`/`then(null, b)`.' 12 | 13 | ruleTester.run('prefer-catch', rule, { 14 | valid: [ 15 | 'prom.then()', 16 | 'prom.then(fn)', 17 | 'prom.then(fn1).then(fn2)', 18 | 'prom.then(() => {})', 19 | 'prom.then(function () {})', 20 | 'prom.catch()', 21 | 'prom.catch(handleErr).then(handle)', 22 | 'prom.catch(handleErr)', 23 | ], 24 | 25 | invalid: [ 26 | { 27 | code: 'hey.then(fn1, fn2)', 28 | errors: [{ message }], 29 | output: 'hey.catch(fn2).then(fn1)', 30 | }, 31 | { 32 | code: 'hey.then(fn1, (fn2))', 33 | errors: [{ message }], 34 | output: 'hey.catch(fn2).then(fn1)', 35 | }, 36 | { 37 | code: 'hey.then(null, fn2)', 38 | errors: [{ message }], 39 | output: 'hey.catch(fn2)', 40 | }, 41 | { 42 | code: 'hey.then(undefined, fn2)', 43 | errors: [{ message }], 44 | output: 'hey.catch(fn2)', 45 | }, 46 | { 47 | code: 'function foo() { hey.then(x => {}, () => {}) }', 48 | errors: [{ message }], 49 | output: 'function foo() { hey.catch(() => {}).then(x => {}) }', 50 | }, 51 | { 52 | code: ` 53 | function foo() { 54 | hey.then(function a() { }, function b() {}).then(fn1, fn2) 55 | } 56 | `, 57 | errors: [{ message }, { message }], 58 | output: ` 59 | function foo() { 60 | hey.catch(function b() {}).then(function a() { }).catch(fn2).then(fn1) 61 | } 62 | `, 63 | }, 64 | ], 65 | }) 66 | -------------------------------------------------------------------------------- /__tests__/no-native.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/no-native') 4 | const { RuleTester } = require('./rule-tester') 5 | const parserOptions = { 6 | ecmaVersion: 6, 7 | sourceType: 'module', 8 | } 9 | const ruleTesters = [ 10 | new RuleTester({ 11 | parserOptions, 12 | }), 13 | new RuleTester({ 14 | parser: require.resolve('@typescript-eslint/parser'), 15 | parserOptions, 16 | }), 17 | ] 18 | 19 | for (const ruleTester of ruleTesters) { 20 | ruleTester.run('no-native', rule, { 21 | valid: [ 22 | 'var Promise = null; function x() { return Promise.resolve("hi"); }', 23 | 'var Promise = window.Promise || require("bluebird"); var x = Promise.reject();', 24 | 'import Promise from "bluebird"; var x = Promise.reject();', 25 | ], 26 | 27 | invalid: [ 28 | { 29 | code: 'new Promise(function(reject, resolve) { })', 30 | errors: [{ message: '"Promise" is not defined.' }], 31 | }, 32 | { 33 | code: 'Promise.resolve()', 34 | errors: [{ message: '"Promise" is not defined.' }], 35 | }, 36 | { 37 | code: 'new Promise(function(reject, resolve) { })', 38 | errors: [{ message: '"Promise" is not defined.' }], 39 | env: { browser: true }, 40 | }, 41 | { 42 | code: 'new Promise(function(reject, resolve) { })', 43 | errors: [{ message: '"Promise" is not defined.' }], 44 | env: { node: true }, 45 | }, 46 | { 47 | code: 'Promise.resolve()', 48 | errors: [{ message: '"Promise" is not defined.' }], 49 | env: { es6: true }, 50 | }, 51 | { 52 | code: 'Promise.resolve()', 53 | errors: [{ message: '"Promise" is not defined.' }], 54 | globals: { Promise: true }, 55 | }, 56 | ], 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /rules/no-promise-in-callback.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rule: no-promise-in-callback 3 | * Discourage using promises inside of callbacks. 4 | */ 5 | 6 | 'use strict' 7 | 8 | const { getAncestors } = require('./lib/eslint-compat') 9 | const getDocsUrl = require('./lib/get-docs-url') 10 | const isPromise = require('./lib/is-promise') 11 | const isInsideCallback = require('./lib/is-inside-callback') 12 | 13 | module.exports = { 14 | meta: { 15 | type: 'suggestion', 16 | docs: { 17 | description: 'Disallow using promises inside of callbacks.', 18 | url: getDocsUrl('no-promise-in-callback'), 19 | }, 20 | schema: [ 21 | { 22 | type: 'object', 23 | properties: { 24 | exemptDeclarations: { 25 | type: 'boolean', 26 | }, 27 | }, 28 | additionalProperties: false, 29 | }, 30 | ], 31 | messages: { 32 | avoidPromiseInCallback: 'Avoid using promises inside of callbacks.', 33 | }, 34 | }, 35 | create(context) { 36 | const { exemptDeclarations = false } = context.options[0] || {} 37 | return { 38 | CallExpression(node) { 39 | if (!isPromise(node)) return 40 | 41 | // if i'm returning the promise, it's probably not really a callback 42 | // function, and I should be okay.... 43 | if (node.parent.type === 'ReturnStatement') return 44 | 45 | // what about if the parent is an ArrowFunctionExpression 46 | // would that imply an implicit return? 47 | 48 | if ( 49 | getAncestors(context, node).some((ancestor) => { 50 | return isInsideCallback(ancestor, exemptDeclarations) 51 | }) 52 | ) { 53 | context.report({ 54 | node: node.callee, 55 | messageId: 'avoidPromiseInCallback', 56 | }) 57 | } 58 | }, 59 | } 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /__tests__/rule-tester.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Helpers for tests. 3 | * @author 唯然 4 | */ 5 | 'use strict' 6 | const { version } = require('eslint/package.json') 7 | const { RuleTester } = require('eslint') 8 | const globals = require('globals') 9 | 10 | const majorVersion = Number.parseInt(version.split('.')[0], 10) 11 | 12 | function convertConfig(config) { 13 | if (config instanceof Object === false) { 14 | return config 15 | } 16 | 17 | if (config.languageOptions == null) { 18 | config.languageOptions = {} 19 | } 20 | 21 | if (config.parserOptions) { 22 | Object.assign(config.languageOptions, config.parserOptions) 23 | delete config.parserOptions 24 | } 25 | 26 | if (typeof config.parser === 'string') { 27 | config.languageOptions.parser = require(config.parser) 28 | delete config.parser 29 | } 30 | 31 | if (config.globals instanceof Object) { 32 | config.languageOptions.globals = config.globals 33 | delete config.globals 34 | } 35 | 36 | if (config.env instanceof Object) { 37 | if (config.languageOptions.globals == null) { 38 | config.languageOptions.globals = {} 39 | } 40 | 41 | for (const key in config.env) { 42 | Object.assign(config.languageOptions.globals, globals[key]) 43 | } 44 | 45 | delete config.env 46 | } 47 | 48 | delete config.parserOptions 49 | delete config.parser 50 | 51 | return config 52 | } 53 | 54 | exports.RuleTester = function (config = {}) { 55 | if (majorVersion <= 8) { 56 | return new RuleTester(config) 57 | } 58 | 59 | const ruleTester = new RuleTester(convertConfig(config)) 60 | const $run = ruleTester.run.bind(ruleTester) 61 | ruleTester.run = function (name, rule, tests) { 62 | tests.valid = tests.valid.map(convertConfig) 63 | tests.invalid = tests.invalid.map(convertConfig) 64 | 65 | $run(name, rule, tests) 66 | } 67 | return ruleTester 68 | } 69 | -------------------------------------------------------------------------------- /rules/prefer-catch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rule: prefer-catch 3 | * Discourage using then(a, b) or then(null, b) and instead use catch(). 4 | */ 5 | 6 | 'use strict' 7 | 8 | const getDocsUrl = require('./lib/get-docs-url') 9 | const { getSourceCode } = require('./lib/eslint-compat') 10 | const removeArgument = require('./fix/remove-argument') 11 | 12 | module.exports = { 13 | meta: { 14 | type: 'suggestion', 15 | docs: { 16 | description: 17 | 'Prefer `catch` to `then(a, b)`/`then(null, b)` for handling errors.', 18 | url: getDocsUrl('prefer-catch'), 19 | }, 20 | fixable: 'code', 21 | schema: [], 22 | messages: { 23 | preferCatchToThen: 'Prefer `catch` to `then(a, b)`/`then(null, b)`.', 24 | }, 25 | }, 26 | create(context) { 27 | const sourceCode = getSourceCode(context) 28 | return { 29 | 'CallExpression > MemberExpression.callee'(node) { 30 | if ( 31 | node.property?.name === 'then' && 32 | node.parent.arguments.length >= 2 33 | ) { 34 | context.report({ 35 | node: node.property, 36 | messageId: 'preferCatchToThen', 37 | *fix(fixer) { 38 | const then = node.parent.arguments[0] 39 | if ( 40 | (then.type === 'Literal' && then.value === null) || 41 | (then.type === 'Identifier' && then.name === 'undefined') 42 | ) { 43 | yield removeArgument(fixer, then, sourceCode) 44 | yield fixer.replaceText(node.property, 'catch') 45 | } else { 46 | const catcher = node.parent.arguments[1] 47 | const catcherText = sourceCode.getText(catcher) 48 | yield removeArgument(fixer, catcher, sourceCode) 49 | yield fixer.insertTextBefore( 50 | node.property, 51 | `catch(${catcherText}).`, 52 | ) 53 | } 54 | }, 55 | }) 56 | } 57 | }, 58 | } 59 | }, 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | lint: 14 | name: ⬣ Lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: ⬇️ Checkout repo 18 | uses: actions/checkout@v4 19 | 20 | - name: ⎔ Setup Node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: lts/* 24 | 25 | - name: 📥 Install dependencies 26 | run: npm ci 27 | 28 | - name: ▶️ Run lint script 29 | run: npm run lint 30 | 31 | test: 32 | name: 33 | 🧪 Test (Node@${{ matrix.node }} - ESLint@${{ matrix.eslint }} - ${{ 34 | matrix.os }}) 35 | strategy: 36 | matrix: 37 | eslint: [8] 38 | node: [18.18.0, 20.9.0, 21.1.0, 22] 39 | os: [ubuntu-latest] 40 | include: 41 | # ESLint v9 42 | - eslint: 9 43 | node: 22 44 | os: ubuntu-latest 45 | # On other platforms 46 | - os: windows-latest 47 | eslint: 8 48 | node: 22 49 | - os: macos-latest 50 | eslint: 8 51 | node: 22 52 | # On the minimum supported ESLint/Node.js version 53 | - eslint: 7.0.0 54 | node: 18.18.0 55 | os: ubuntu-latest 56 | runs-on: ${{ matrix.os }} 57 | steps: 58 | - name: ⬇️ Checkout repo 59 | uses: actions/checkout@v4 60 | 61 | - name: ⎔ Setup Node v${{ matrix.node }} 62 | uses: actions/setup-node@v4 63 | with: 64 | cache: npm 65 | node-version: ${{ matrix.node }} 66 | 67 | - name: 📥 Install dependencies 68 | run: npm ci 69 | 70 | - name: 📥 Install ESLint v${{ matrix.eslint }} 71 | run: npm install eslint@${{ matrix.eslint }} 72 | 73 | - name: ▶️ Run test script 74 | run: npm run test -- --runInBand 75 | 76 | - name: ⬆️ Upload coverage report 77 | uses: codecov/codecov-action@v4 78 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const recommendedRules = { 4 | 'promise/always-return': 'error', 5 | 'promise/no-return-wrap': 'error', 6 | 'promise/param-names': 'error', 7 | 'promise/catch-or-return': 'error', 8 | 'promise/no-native': 'off', 9 | 'promise/no-nesting': 'warn', 10 | 'promise/no-promise-in-callback': 'warn', 11 | 'promise/no-callback-in-promise': 'warn', 12 | 'promise/avoid-new': 'off', 13 | 'promise/no-new-statics': 'error', 14 | 'promise/no-return-in-finally': 'warn', 15 | 'promise/valid-params': 'warn', 16 | } 17 | 18 | const pluginPromise = { 19 | rules: { 20 | 'param-names': require('./rules/param-names'), 21 | 'no-return-wrap': require('./rules/no-return-wrap'), 22 | 'always-return': require('./rules/always-return'), 23 | 'catch-or-return': require('./rules/catch-or-return'), 24 | 'prefer-await-to-callbacks': require('./rules/prefer-await-to-callbacks'), 25 | 'prefer-await-to-then': require('./rules/prefer-await-to-then'), 26 | 'prefer-catch': require('./rules/prefer-catch'), 27 | 'no-native': require('./rules/no-native'), 28 | 'no-callback-in-promise': require('./rules/no-callback-in-promise'), 29 | 'no-promise-in-callback': require('./rules/no-promise-in-callback'), 30 | 'no-nesting': require('./rules/no-nesting'), 31 | 'avoid-new': require('./rules/avoid-new'), 32 | 'no-new-statics': require('./rules/no-new-statics'), 33 | 'no-return-in-finally': require('./rules/no-return-in-finally'), 34 | 'valid-params': require('./rules/valid-params'), 35 | 'no-multiple-resolved': require('./rules/no-multiple-resolved'), 36 | 'spec-only': require('./rules/spec-only'), 37 | }, 38 | rulesConfig: { 39 | 'param-names': 1, 40 | 'always-return': 1, 41 | 'no-return-wrap': 1, 42 | 'no-native': 0, 43 | 'catch-or-return': 1, 44 | }, 45 | } 46 | pluginPromise.configs = { 47 | recommended: { 48 | plugins: ['promise'], 49 | rules: recommendedRules, 50 | }, 51 | 'flat/recommended': { 52 | name: 'promise/flat/recommended', 53 | plugins: { promise: pluginPromise }, 54 | rules: recommendedRules, 55 | }, 56 | } 57 | module.exports = pluginPromise 58 | -------------------------------------------------------------------------------- /__tests__/param-names.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/param-names') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 6, 8 | }, 9 | }) 10 | 11 | const messageForResolve = 12 | 'Promise constructor parameters must be named to match "^_?resolve$"' 13 | const messageForReject = 14 | 'Promise constructor parameters must be named to match "^_?reject$"' 15 | 16 | ruleTester.run('param-names', rule, { 17 | valid: [ 18 | 'new Promise(function(resolve, reject) {})', 19 | 'new Promise(function(resolve, _reject) {})', 20 | 'new Promise(function(_resolve, reject) {})', 21 | 'new Promise(function(_resolve, _reject) {})', 22 | 'new Promise(function(resolve) {})', 23 | 'new Promise(function(_resolve) {})', 24 | 'new Promise(resolve => {})', 25 | 'new Promise((resolve, reject) => {})', 26 | 'new Promise(() => {})', 27 | 'new NonPromise()', 28 | { 29 | code: 'new Promise((yes, no) => {})', 30 | options: [{ resolvePattern: '^yes$', rejectPattern: '^no$' }], 31 | }, 32 | ], 33 | 34 | invalid: [ 35 | { 36 | code: 'new Promise(function(reject, resolve) {})', 37 | errors: [{ message: messageForResolve }, { message: messageForReject }], 38 | }, 39 | { 40 | code: 'new Promise(function(resolve, rej) {})', 41 | errors: [{ message: messageForReject }], 42 | }, 43 | { 44 | code: 'new Promise(yes => {})', 45 | errors: [{ message: messageForResolve }], 46 | }, 47 | { 48 | code: 'new Promise((yes, no) => {})', 49 | errors: [{ message: messageForResolve }, { message: messageForReject }], 50 | }, 51 | { 52 | code: 'new Promise(function(resolve, reject) {})', 53 | options: [{ resolvePattern: '^yes$', rejectPattern: '^no$' }], 54 | errors: [ 55 | { 56 | message: 57 | 'Promise constructor parameters must be named to match "^yes$"', 58 | }, 59 | { 60 | message: 61 | 'Promise constructor parameters must be named to match "^no$"', 62 | }, 63 | ], 64 | }, 65 | ], 66 | }) 67 | -------------------------------------------------------------------------------- /__tests__/no-new-statics.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/no-new-statics') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester() 6 | 7 | ruleTester.run('no-new-statics', rule, { 8 | valid: [ 9 | 'Promise.resolve()', 10 | 'Promise.reject()', 11 | 'Promise.all()', 12 | 'Promise.race()', 13 | 'Promise.withResolvers()', 14 | 'new Promise(function (resolve, reject) {})', 15 | 'new SomeClass()', 16 | 'SomeClass.resolve()', 17 | 'new SomeClass.resolve()', 18 | ], 19 | invalid: [ 20 | { 21 | code: 'new Promise.resolve()', 22 | output: 'Promise.resolve()', 23 | errors: [{ message: "Avoid calling 'new' on 'Promise.resolve()'" }], 24 | }, 25 | { 26 | code: 'new Promise.reject()', 27 | output: 'Promise.reject()', 28 | errors: [{ message: "Avoid calling 'new' on 'Promise.reject()'" }], 29 | }, 30 | { 31 | code: 'new Promise.all()', 32 | output: 'Promise.all()', 33 | errors: [{ message: "Avoid calling 'new' on 'Promise.all()'" }], 34 | }, 35 | { 36 | code: 'new Promise.allSettled()', 37 | output: 'Promise.allSettled()', 38 | errors: [{ message: "Avoid calling 'new' on 'Promise.allSettled()'" }], 39 | }, 40 | { 41 | code: 'new Promise.any()', 42 | output: 'Promise.any()', 43 | errors: [{ message: "Avoid calling 'new' on 'Promise.any()'" }], 44 | }, 45 | { 46 | code: 'new Promise.race()', 47 | output: 'Promise.race()', 48 | errors: [{ message: "Avoid calling 'new' on 'Promise.race()'" }], 49 | }, 50 | { 51 | code: 'new Promise.withResolvers()', 52 | output: 'Promise.withResolvers()', 53 | errors: [{ message: "Avoid calling 'new' on 'Promise.withResolvers()'" }], 54 | }, 55 | { 56 | code: ` 57 | function foo() { 58 | var a = getA() 59 | return new Promise.resolve(a) 60 | } 61 | `, 62 | output: ` 63 | function foo() { 64 | var a = getA() 65 | return Promise.resolve(a) 66 | } 67 | `, 68 | errors: [{ message: "Avoid calling 'new' on 'Promise.resolve()'" }], 69 | }, 70 | ], 71 | }) 72 | -------------------------------------------------------------------------------- /__tests__/spec-only.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/spec-only') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester() 6 | 7 | ruleTester.run('spec-only', rule, { 8 | valid: [ 9 | 'Promise.resolve()', 10 | 'Promise.reject()', 11 | 'Promise.all()', 12 | 'Promise["all"]', 13 | 'Promise[method];', 14 | 'Promise.prototype;', 15 | 'Promise.prototype[method];', 16 | 'Promise.prototype["then"];', 17 | 'Promise.race()', 18 | 'var ctch = Promise.prototype.catch', 19 | 'Promise.withResolvers()', 20 | 'new Promise(function (resolve, reject) {})', 21 | 'SomeClass.resolve()', 22 | 'doSomething(Promise.all)', 23 | { 24 | code: 'Promise.permittedMethod()', 25 | options: [ 26 | { 27 | allowedMethods: ['permittedMethod'], 28 | }, 29 | ], 30 | }, 31 | { 32 | code: 'Promise.prototype.permittedInstanceMethod', 33 | options: [ 34 | { 35 | allowedMethods: ['permittedInstanceMethod'], 36 | }, 37 | ], 38 | }, 39 | ], 40 | invalid: [ 41 | { 42 | code: 'Promise.done()', 43 | errors: [{ message: "Avoid using non-standard 'Promise.done'" }], 44 | }, 45 | { 46 | code: 'Promise.something()', 47 | errors: [{ message: "Avoid using non-standard 'Promise.something'" }], 48 | }, 49 | { 50 | code: 'new Promise.done()', 51 | errors: [{ message: "Avoid using non-standard 'Promise.done'" }], 52 | }, 53 | { 54 | code: ` 55 | function foo() { 56 | var a = getA() 57 | return Promise.done(a) 58 | } 59 | `, 60 | errors: [{ message: "Avoid using non-standard 'Promise.done'" }], 61 | }, 62 | { 63 | code: ` 64 | function foo() { 65 | getA(Promise.done) 66 | } 67 | `, 68 | errors: [{ message: "Avoid using non-standard 'Promise.done'" }], 69 | }, 70 | { 71 | code: `var done = Promise.prototype.done`, 72 | errors: [{ message: "Avoid using non-standard 'Promise.prototype'" }], 73 | }, 74 | { 75 | code: `Promise["done"];`, 76 | errors: [{ message: "Avoid using non-standard 'Promise.done'" }], 77 | }, 78 | ], 79 | }) 80 | -------------------------------------------------------------------------------- /rules/no-native.js: -------------------------------------------------------------------------------- 1 | // Borrowed from here: 2 | // https://github.com/colonyamerican/eslint-plugin-cah/issues/3 3 | 4 | 'use strict' 5 | 6 | const { getScope } = require('./lib/eslint-compat') 7 | const getDocsUrl = require('./lib/get-docs-url') 8 | 9 | function isDeclared(scope, ref) { 10 | return scope.variables.some((variable) => { 11 | if (variable.name !== ref.identifier.name) { 12 | return false 13 | } 14 | 15 | // Presumably can't pass this since the implicit `Promise` global 16 | // being checked here would always lack `defs` 17 | // istanbul ignore else 18 | if (!variable.defs || !variable.defs.length) { 19 | return false 20 | } 21 | 22 | // istanbul ignore next 23 | return true 24 | }) 25 | } 26 | 27 | module.exports = { 28 | meta: { 29 | type: 'suggestion', 30 | docs: { 31 | description: 32 | 'Require creating a `Promise` constructor before using it in an ES5 environment.', 33 | url: getDocsUrl('no-native'), 34 | }, 35 | messages: { 36 | name: '"{{name}}" is not defined.', 37 | }, 38 | schema: [], 39 | }, 40 | create(context) { 41 | /** 42 | * Checks for and reports reassigned constants 43 | * 44 | * @param {Scope} scope - an eslint-scope Scope object 45 | * @returns {void} 46 | * @private 47 | */ 48 | return { 49 | 'Program:exit'(node) { 50 | const scope = getScope(context, node) 51 | const leftToBeResolved = 52 | scope.implicit.left || 53 | /** 54 | * Fixes https://github.com/eslint-community/eslint-plugin-promise/issues/205. 55 | * The problem was that @typescript-eslint has a scope manager 56 | * which has `leftToBeResolved` instead of the default `left`. 57 | */ 58 | scope.implicit.leftToBeResolved 59 | 60 | leftToBeResolved.forEach((ref) => { 61 | if (ref.identifier.name !== 'Promise') { 62 | return 63 | } 64 | 65 | // istanbul ignore else 66 | if (!isDeclared(scope, ref)) { 67 | context.report({ 68 | node: ref.identifier, 69 | messageId: 'name', 70 | data: { name: ref.identifier.name }, 71 | }) 72 | } 73 | }) 74 | }, 75 | } 76 | }, 77 | } 78 | -------------------------------------------------------------------------------- /rules/param-names.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const getDocsUrl = require('./lib/get-docs-url') 4 | const { 5 | isPromiseConstructorWithInlineExecutor, 6 | } = require('./lib/is-promise-constructor') 7 | 8 | module.exports = { 9 | meta: { 10 | type: 'suggestion', 11 | docs: { 12 | description: 13 | 'Enforce consistent param names and ordering when creating new promises.', 14 | url: getDocsUrl('param-names'), 15 | }, 16 | schema: [ 17 | { 18 | type: 'object', 19 | properties: { 20 | resolvePattern: { type: 'string' }, 21 | rejectPattern: { type: 'string' }, 22 | }, 23 | additionalProperties: false, 24 | }, 25 | ], 26 | messages: { 27 | resolveParamNames: 28 | 'Promise constructor parameters must be named to match "{{ resolvePattern }}"', 29 | rejectParamNames: 30 | 'Promise constructor parameters must be named to match "{{ rejectPattern }}"', 31 | }, 32 | }, 33 | create(context) { 34 | const options = context.options[0] || {} 35 | const resolvePattern = new RegExp( 36 | options.resolvePattern || '^_?resolve$', 37 | 'u', 38 | ) 39 | const rejectPattern = new RegExp(options.rejectPattern || '^_?reject$', 'u') 40 | 41 | return { 42 | NewExpression(node) { 43 | if (isPromiseConstructorWithInlineExecutor(node)) { 44 | const params = node.arguments[0].params 45 | 46 | if (!params || !params.length) { 47 | return 48 | } 49 | 50 | const resolveParamName = params[0] && params[0].name 51 | if (resolveParamName && !resolvePattern.test(resolveParamName)) { 52 | context.report({ 53 | node: params[0], 54 | messageId: 'resolveParamNames', 55 | data: { 56 | resolvePattern: resolvePattern.source, 57 | }, 58 | }) 59 | } 60 | const rejectParamName = params[1] && params[1].name 61 | if (rejectParamName && !rejectPattern.test(rejectParamName)) { 62 | context.report({ 63 | node: params[1], 64 | messageId: 'rejectParamNames', 65 | data: { 66 | rejectPattern: rejectPattern.source, 67 | }, 68 | }) 69 | } 70 | } 71 | }, 72 | } 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /docs/rules/valid-params.md: -------------------------------------------------------------------------------- 1 | # Enforces the proper number of arguments are passed to Promise functions (`promise/valid-params`) 2 | 3 | ⚠️ This rule _warns_ in the following configs: ✅ `flat/recommended`, ✅ 4 | `recommended`. 5 | 6 | 7 | 8 | Calling a Promise function with the incorrect number of arguments can lead to 9 | unexpected behavior or hard to spot bugs. 10 | 11 | ## Rule Details 12 | 13 | This rule is aimed at flagging instances where a Promise function is called with 14 | the improper number of arguments. 15 | 16 | Examples of **incorrect** code for this rule: 17 | 18 | - `Promise.all()` is called with 0 or 2+ arguments 19 | - `Promise.race()` is called with 0 or 2+ arguments 20 | - `Promise.resolve()` is called with 2+ arguments 21 | - `Promise.reject()` is called with 2+ arguments 22 | - `Promise.then()` is called with 0 or 3+ arguments 23 | - `Promise.catch()` is called with 0 or 2+ arguments 24 | - `Promise.finally()` is called with 0 or 2+ arguments 25 | 26 | Examples of **correct** code for this rule: 27 | 28 | ```js 29 | // Promise.all() requires 1 argument 30 | Promise.all([p1, p2, p3]) 31 | Promise.all(iterable) 32 | 33 | // Promise.race() requires 1 argument 34 | Promise.race([p1, p2, p3]) 35 | Promise.race(iterable) 36 | 37 | // Promise.resolve() requires 0 or 1 arguments 38 | Promise.resolve() 39 | Promise.resolve({}) 40 | Promise.resolve([1, 2, 3]) 41 | Promise.resolve(referenceToObject) 42 | 43 | // Promise.reject() requires 0 or 1 arguments 44 | Promise.reject() 45 | Promise.reject(Error()) 46 | Promise.reject(referenceToError) 47 | 48 | // Promise.then() requires 1 or 2 arguments 49 | somePromise().then((value) => doSomething(value)) 50 | somePromise().then(successCallback, errorCallback) 51 | 52 | // Promise.catch() requires 1 argument 53 | somePromise().catch((error) => { 54 | handleError(error) 55 | }) 56 | somePromise().catch(console.error) 57 | 58 | // Promise.finally() requires 1 argument 59 | somePromise().finally(() => { 60 | console.log('done!') 61 | }) 62 | somePromise().finally(console.log) 63 | ``` 64 | 65 | ## Options 66 | 67 | ### `exclude` 68 | 69 | Array of method names to exclude from checks. Defaults to an empty array. 70 | 71 | ## When Not To Use It 72 | 73 | If you do not want to be notified when passing an invalid number of arguments to 74 | a Promise function (for example, when using a typechecker like Flow), you can 75 | safely disable this rule. 76 | -------------------------------------------------------------------------------- /rules/spec-only.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PROMISE_STATICS = require('./lib/promise-statics') 4 | const getDocsUrl = require('./lib/get-docs-url') 5 | 6 | const PROMISE_INSTANCE_METHODS = new Set(['then', 'catch', 'finally']) 7 | 8 | function isPermittedProperty(expression, standardSet, allowedMethods) { 9 | // istanbul ignore if 10 | if (expression.type !== 'MemberExpression') return false 11 | 12 | if (expression.property.type === 'Literal') 13 | return ( 14 | standardSet.has(expression.property.value) || 15 | allowedMethods.includes(expression.property.value) 16 | ) 17 | 18 | // istanbul ignore else 19 | if (expression.property.type === 'Identifier') 20 | return ( 21 | expression.computed || 22 | standardSet.has(expression.property.name) || 23 | allowedMethods.includes(expression.property.name) 24 | ) 25 | 26 | // istanbul ignore next 27 | return false 28 | } 29 | 30 | module.exports = { 31 | meta: { 32 | type: 'problem', 33 | docs: { 34 | description: 'Disallow use of non-standard Promise static methods.', 35 | url: getDocsUrl('spec-only'), 36 | }, 37 | schema: [ 38 | { 39 | type: 'object', 40 | properties: { 41 | allowedMethods: { 42 | type: 'array', 43 | items: { 44 | type: 'string', 45 | }, 46 | }, 47 | }, 48 | additionalProperties: false, 49 | }, 50 | ], 51 | messages: { 52 | avoidNonStandard: "Avoid using non-standard 'Promise.{{ name }}'", 53 | }, 54 | }, 55 | create(context) { 56 | const { allowedMethods = [] } = context.options[0] || {} 57 | 58 | return { 59 | MemberExpression(node) { 60 | if ( 61 | node.object.type === 'Identifier' && 62 | node.object.name === 'Promise' && 63 | ((node.property.name !== 'prototype' && 64 | !isPermittedProperty(node, PROMISE_STATICS, allowedMethods)) || 65 | (node.property.name === 'prototype' && 66 | node.parent.type === 'MemberExpression' && 67 | !isPermittedProperty( 68 | node.parent, 69 | PROMISE_INSTANCE_METHODS, 70 | allowedMethods, 71 | ))) 72 | ) { 73 | context.report({ 74 | node, 75 | messageId: 'avoidNonStandard', 76 | data: { name: node.property.name ?? node.property.value }, 77 | }) 78 | } 79 | }, 80 | } 81 | }, 82 | } 83 | -------------------------------------------------------------------------------- /rules/valid-params.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const getDocsUrl = require('./lib/get-docs-url') 4 | const isPromise = require('./lib/is-promise') 5 | 6 | module.exports = { 7 | meta: { 8 | type: 'problem', 9 | docs: { 10 | description: 11 | 'Enforces the proper number of arguments are passed to Promise functions.', 12 | url: getDocsUrl('valid-params'), 13 | }, 14 | schema: [ 15 | { 16 | type: 'object', 17 | properties: { 18 | exclude: { 19 | type: 'array', 20 | items: { 21 | type: 'string', 22 | }, 23 | }, 24 | }, 25 | additionalProperties: false, 26 | }, 27 | ], 28 | messages: { 29 | requireOneOptionalArgument: 30 | 'Promise.{{ name }}() requires 0 or 1 arguments, but received {{ numArgs }}', 31 | requireOneArgument: 32 | 'Promise.{{ name }}() requires 1 argument, but received {{ numArgs }}', 33 | requireTwoOptionalArguments: 34 | 'Promise.{{ name }}() requires 1 or 2 arguments, but received {{ numArgs }}', 35 | }, 36 | }, 37 | create(context) { 38 | const { exclude = [] } = context.options[0] || {} 39 | return { 40 | CallExpression(node) { 41 | if (!isPromise(node)) { 42 | return 43 | } 44 | 45 | const name = node.callee.property.name 46 | const numArgs = node.arguments.length 47 | 48 | if (exclude.includes(name)) { 49 | return 50 | } 51 | 52 | // istanbul ignore next -- `isPromise` filters out others 53 | switch (name) { 54 | case 'resolve': 55 | case 'reject': 56 | if (numArgs > 1) { 57 | context.report({ 58 | node, 59 | messageId: 'requireOneOptionalArgument', 60 | data: { name, numArgs }, 61 | }) 62 | } 63 | break 64 | case 'then': 65 | if (numArgs < 1 || numArgs > 2) { 66 | context.report({ 67 | node, 68 | messageId: 'requireTwoOptionalArguments', 69 | data: { name, numArgs }, 70 | }) 71 | } 72 | break 73 | case 'race': 74 | case 'all': 75 | case 'allSettled': 76 | case 'any': 77 | case 'catch': 78 | case 'finally': 79 | if (numArgs !== 1) { 80 | context.report({ 81 | node, 82 | messageId: 'requireOneArgument', 83 | data: { name, numArgs }, 84 | }) 85 | } 86 | break 87 | default: 88 | // istanbul ignore next -- `isPromise` filters out others 89 | break 90 | } 91 | }, 92 | } 93 | }, 94 | } 95 | -------------------------------------------------------------------------------- /rules/prefer-await-to-then.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rule: prefer-await-to-then 3 | * Discourage using then()/catch()/finally() and instead use async/await. 4 | */ 5 | 6 | 'use strict' 7 | 8 | const { getAncestors, getScope } = require('./lib/eslint-compat') 9 | const getDocsUrl = require('./lib/get-docs-url') 10 | const isMemberCallWithObjectName = require('./lib/is-member-call-with-object-name') 11 | 12 | module.exports = { 13 | meta: { 14 | type: 'suggestion', 15 | docs: { 16 | description: 17 | 'Prefer `await` to `then()`/`catch()`/`finally()` for reading Promise values.', 18 | url: getDocsUrl('prefer-await-to-then'), 19 | }, 20 | schema: [ 21 | { 22 | type: 'object', 23 | properties: { 24 | strict: { 25 | type: 'boolean', 26 | }, 27 | }, 28 | }, 29 | ], 30 | messages: { 31 | preferAwaitToCallback: 'Prefer await to then()/catch()/finally().', 32 | }, 33 | }, 34 | create(context) { 35 | /** Returns true if node is inside yield or await expression. */ 36 | function isInsideYieldOrAwait(node) { 37 | return getAncestors(context, node).some((parent) => { 38 | return ( 39 | parent.type === 'AwaitExpression' || parent.type === 'YieldExpression' 40 | ) 41 | }) 42 | } 43 | 44 | /** Returns true if node is inside a constructor */ 45 | function isInsideConstructor(node) { 46 | return getAncestors(context, node).some((parent) => { 47 | return ( 48 | parent.type === 'MethodDefinition' && parent.kind === 'constructor' 49 | ) 50 | }) 51 | } 52 | 53 | /** 54 | * Returns true if node is created at the top-level scope. 55 | * Await statements are not allowed at the top level, 56 | * only within function declarations. 57 | */ 58 | function isTopLevelScoped(node) { 59 | return getScope(context, node).block.type === 'Program' 60 | } 61 | 62 | const { strict } = context.options[0] || {} 63 | 64 | return { 65 | 'CallExpression > MemberExpression.callee'(node) { 66 | if ( 67 | isTopLevelScoped(node) || 68 | (!strict && isInsideYieldOrAwait(node)) || 69 | (!strict && isInsideConstructor(node)) || 70 | isMemberCallWithObjectName('cy', node.parent) 71 | ) { 72 | return 73 | } 74 | 75 | // if you're a then/catch/finally expression then you're probably a promise 76 | if ( 77 | node.property && 78 | (node.property.name === 'then' || 79 | node.property.name === 'catch' || 80 | node.property.name === 'finally') 81 | ) { 82 | context.report({ 83 | node: node.property, 84 | messageId: 'preferAwaitToCallback', 85 | }) 86 | } 87 | }, 88 | } 89 | }, 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-promise", 3 | "version": "7.2.1", 4 | "description": "Enforce best practices for JavaScript promises", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "keywords": [ 8 | "eslint", 9 | "eslintplugin", 10 | "eslint-plugin", 11 | "promise", 12 | "promises" 13 | ], 14 | "homepage": "https://github.com/eslint-community/eslint-plugin-promise", 15 | "bugs": "https://github.com/eslint-community/eslint-plugin-promise/issues", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/eslint-community/eslint-plugin-promise" 19 | }, 20 | "license": "ISC", 21 | "author": "jden ", 22 | "contributors": [ 23 | "Brett Zamir", 24 | "Aadit M Shah (https://aadit.codes/)" 25 | ], 26 | "scripts": { 27 | "format": "prettier --write . && eslint . --fix", 28 | "lint": "npm-run-all \"lint:*\"", 29 | "lint:eslint-docs": "npm run update:eslint-docs && git diff --exit-code", 30 | "lint:js": "eslint --report-unused-disable-directives .", 31 | "prepare": "husky", 32 | "test": "jest --coverage", 33 | "update:eslint-docs": "eslint-doc-generator && npm run format" 34 | }, 35 | "lint-staged": { 36 | "{README.md,CONTRIBUTING.md}": [ 37 | "doctoc --maxlevel 3 --notitle" 38 | ], 39 | "*.js": [ 40 | "prettier --write", 41 | "eslint --report-unused-disable-directives --fix" 42 | ], 43 | "*.+(json|md)": [ 44 | "prettier --write" 45 | ] 46 | }, 47 | "prettier": { 48 | "proseWrap": "always", 49 | "semi": false, 50 | "singleQuote": true 51 | }, 52 | "jest": { 53 | "coverageThreshold": { 54 | "global": { 55 | "branches": 100, 56 | "functions": 100, 57 | "lines": 100, 58 | "statements": 100 59 | } 60 | }, 61 | "collectCoverageFrom": [ 62 | "rules/*.js", 63 | "rules/*/*.js", 64 | "!rules/lib/eslint-compat.js" 65 | ], 66 | "testPathIgnorePatterns": [ 67 | "__tests__/rule-tester.js" 68 | ] 69 | }, 70 | "devDependencies": { 71 | "@typescript-eslint/parser": "^7.17.0", 72 | "doctoc": "^2.2.1", 73 | "eslint": "^8.56.0", 74 | "eslint-config-prettier": "^9.1.0", 75 | "eslint-doc-generator": "^1.7.1", 76 | "eslint-plugin-eslint-plugin": "^6.2.0", 77 | "eslint-plugin-jest": "^28.6.0", 78 | "eslint-plugin-n": "^17.9.0", 79 | "eslint-plugin-prettier": "^5.2.1", 80 | "globals": "^15.8.0", 81 | "husky": "^9.1.1", 82 | "jest": "^29.7.0", 83 | "lint-staged": "^15.2.7", 84 | "npm-run-all2": "^6.2.2", 85 | "prettier": "^3.3.3", 86 | "typescript": "~5.7.0" 87 | }, 88 | "peerDependencies": { 89 | "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" 90 | }, 91 | "engines": { 92 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 93 | }, 94 | "funding": "https://opencollective.com/eslint", 95 | "dependencies": { 96 | "@eslint-community/eslint-utils": "^4.4.0" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /__tests__/prefer-await-to-callbacks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/prefer-await-to-callbacks') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 8, 8 | }, 9 | }) 10 | 11 | const message = 'Avoid callbacks. Prefer Async/Await.' 12 | 13 | ruleTester.run('prefer-await-to-callbacks', rule, { 14 | valid: [ 15 | 'async function hi() { await thing().catch(err => console.log(err)) }', 16 | 'async function hi() { await thing().then() }', 17 | 'async function hi() { await thing().catch() }', 18 | 'dbConn.on("error", err => { console.error(err) })', 19 | 'dbConn.once("error", err => { console.error(err) })', 20 | 'heart(something => {})', 21 | 'getErrors().map(error => responseTo(error))', 22 | 'errors.filter(err => err.status === 402)', 23 | 'errors.some(err => err.message.includes("Yo"))', 24 | 'errors.every(err => err.status === 402)', 25 | 'errors.filter(err => console.log(err))', 26 | 'const error = errors.find(err => err.stack.includes("file.js"))', 27 | 'this.myErrors.forEach(function(error) { log(error); })', 28 | 'find(errors, function(err) { return err.type === "CoolError" })', 29 | 'map(errors, function(error) { return err.type === "CoolError" })', 30 | '_.find(errors, function(error) { return err.type === "CoolError" })', 31 | '_.map(errors, function(err) { return err.type === "CoolError" })', 32 | ], 33 | 34 | invalid: [ 35 | { 36 | code: 'heart(function(err) {})', 37 | errors: [{ message }], 38 | }, 39 | { 40 | code: 'heart(err => {})', 41 | errors: [{ message }], 42 | }, 43 | { 44 | code: 'heart("ball", function(err) {})', 45 | errors: [{ message }], 46 | }, 47 | { 48 | code: 'function getData(id, callback) {}', 49 | errors: [{ message }], 50 | }, 51 | { 52 | code: 'const getData = (cb) => {}', 53 | errors: [{ message }], 54 | }, 55 | { 56 | code: 'var x = function (x, cb) {}', 57 | errors: [{ message }], 58 | }, 59 | { 60 | code: 'cb()', 61 | errors: [{ message }], 62 | }, 63 | { 64 | code: 'callback()', 65 | errors: [{ message }], 66 | }, 67 | { 68 | code: 'heart(error => {})', 69 | errors: [{ message }], 70 | }, 71 | { 72 | code: `async.map(files, fs.stat, function(err, results) { if (err) throw err; });`, 73 | errors: [{ message }], 74 | }, 75 | { 76 | code: `_.map(files, fs.stat, function(err, results) { if (err) throw err; });`, 77 | errors: [{ message }], 78 | }, 79 | { 80 | code: `map(files, fs.stat, function(err, results) { if (err) throw err; });`, 81 | errors: [{ message }], 82 | }, 83 | { 84 | code: `map(function(err, results) { if (err) throw err; });`, 85 | errors: [{ message }], 86 | }, 87 | { 88 | code: `customMap(errors, (err) => err.message)`, 89 | errors: [{ message }], 90 | }, 91 | ], 92 | }) 93 | -------------------------------------------------------------------------------- /docs/rules/always-return.md: -------------------------------------------------------------------------------- 1 | # Require returning inside each `then()` to create readable and reusable Promise chains (`promise/always-return`) 2 | 3 | 💼 This rule is enabled in the following configs: ✅ `flat/recommended`, ✅ 4 | `recommended`. 5 | 6 | 7 | 8 | Ensure that inside a `then()` you make sure to `return` a new promise or value. 9 | See http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html (rule #5) 10 | for more info on why that's a good idea. 11 | 12 | We also allow someone to `throw` inside a `then()` which is essentially the same 13 | as `return Promise.reject()`. 14 | 15 | #### Valid 16 | 17 | ```js 18 | myPromise.then((val) => val * 2) 19 | myPromise.then(function (val) { 20 | return val * 2 21 | }) 22 | myPromise.then(doSomething) // could be either 23 | myPromise.then((b) => { 24 | if (b) { 25 | return 'yes' 26 | } else { 27 | return 'no' 28 | } 29 | }) 30 | ``` 31 | 32 | #### Invalid 33 | 34 | ```js 35 | myPromise.then(function (val) {}) 36 | myPromise.then(() => { 37 | doSomething() 38 | }) 39 | myPromise.then((b) => { 40 | if (b) { 41 | return 'yes' 42 | } else { 43 | forgotToReturn() 44 | } 45 | }) 46 | ``` 47 | 48 | #### Options 49 | 50 | ##### `ignoreLastCallback` 51 | 52 | You can pass an `{ ignoreLastCallback: true }` as an option to this rule so that 53 | the last `then()` callback in a promise chain does not warn if it does not have 54 | a `return`. Default is `false`. 55 | 56 | ```js 57 | // OK 58 | promise.then((x) => { 59 | console.log(x) 60 | }) 61 | // OK 62 | void promise.then((x) => { 63 | console.log(x) 64 | }) 65 | // OK 66 | await promise.then((x) => { 67 | console.log(x) 68 | }) 69 | 70 | promise 71 | // NG 72 | .then((x) => { 73 | console.log(x) 74 | }) 75 | // OK 76 | .then((x) => { 77 | console.log(x) 78 | }) 79 | 80 | // NG 81 | const v = promise.then((x) => { 82 | console.log(x) 83 | }) 84 | // NG 85 | const v = await promise.then((x) => { 86 | console.log(x) 87 | }) 88 | function foo() { 89 | // NG 90 | return promise.then((x) => { 91 | console.log(x) 92 | }) 93 | } 94 | ``` 95 | 96 | ##### `ignoreAssignmentVariable` 97 | 98 | You can pass an `{ ignoreAssignmentVariable: [] }` as an option to this rule 99 | with a list of variable names so that the last `then()` callback in a promise 100 | chain does not warn if it does an assignment to a global variable. Default is 101 | `["globalThis"]`. 102 | 103 | ```js 104 | /* eslint promise/always-return: ["error", { ignoreAssignmentVariable: ["globalThis"] }] */ 105 | 106 | // OK 107 | promise.then((x) => { 108 | globalThis = x 109 | }) 110 | 111 | promise.then((x) => { 112 | globalThis.x = x 113 | }) 114 | 115 | // OK 116 | promise.then((x) => { 117 | globalThis.x.y = x 118 | }) 119 | 120 | // NG 121 | promise.then((x) => { 122 | anyOtherVariable = x 123 | }) 124 | 125 | // NG 126 | promise.then((x) => { 127 | anyOtherVariable.x = x 128 | }) 129 | 130 | // NG 131 | promise.then((x) => { 132 | x() 133 | }) 134 | ``` 135 | -------------------------------------------------------------------------------- /rules/no-return-wrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rule: no-return-wrap function 3 | * Prevents unnecessary wrapping of results in Promise.resolve 4 | * or Promise.reject as the Promise will do that for us 5 | */ 6 | 7 | 'use strict' 8 | 9 | const { getAncestors } = require('./lib/eslint-compat') 10 | const getDocsUrl = require('./lib/get-docs-url') 11 | const isPromise = require('./lib/is-promise') 12 | 13 | function isInPromise(context, node) { 14 | let functionNode = getAncestors(context, node) 15 | .filter((node) => { 16 | return ( 17 | node.type === 'ArrowFunctionExpression' || 18 | node.type === 'FunctionExpression' 19 | ) 20 | }) 21 | .reverse()[0] 22 | while ( 23 | functionNode && 24 | functionNode.parent && 25 | functionNode.parent.type === 'MemberExpression' && 26 | functionNode.parent.object === functionNode && 27 | functionNode.parent.property.type === 'Identifier' && 28 | functionNode.parent.property.name === 'bind' && 29 | functionNode.parent.parent && 30 | functionNode.parent.parent.type === 'CallExpression' && 31 | functionNode.parent.parent.callee === functionNode.parent 32 | ) { 33 | functionNode = functionNode.parent.parent 34 | } 35 | return functionNode && functionNode.parent && isPromise(functionNode.parent) 36 | } 37 | 38 | module.exports = { 39 | meta: { 40 | type: 'suggestion', 41 | docs: { 42 | description: 43 | 'Disallow wrapping values in `Promise.resolve` or `Promise.reject` when not needed.', 44 | url: getDocsUrl('no-return-wrap'), 45 | }, 46 | messages: { 47 | resolve: 'Avoid wrapping return values in Promise.resolve', 48 | reject: 'Expected throw instead of Promise.reject', 49 | }, 50 | schema: [ 51 | { 52 | type: 'object', 53 | properties: { 54 | allowReject: { 55 | type: 'boolean', 56 | }, 57 | }, 58 | additionalProperties: false, 59 | }, 60 | ], 61 | }, 62 | create(context) { 63 | const options = context.options[0] || {} 64 | const allowReject = options.allowReject 65 | 66 | /** 67 | * Checks a call expression, reporting if necessary. 68 | * @param callExpression The call expression. 69 | * @param node The node to report. 70 | */ 71 | function checkCallExpression({ callee }, node) { 72 | if ( 73 | isInPromise(context, node) && 74 | callee.type === 'MemberExpression' && 75 | callee.object.name === 'Promise' 76 | ) { 77 | if (callee.property.name === 'resolve') { 78 | context.report({ node, messageId: 'resolve' }) 79 | } else if (!allowReject && callee.property.name === 'reject') { 80 | context.report({ node, messageId: 'reject' }) 81 | } 82 | } 83 | } 84 | 85 | return { 86 | ReturnStatement(node) { 87 | if (node.argument && node.argument.type === 'CallExpression') { 88 | checkCallExpression(node.argument, node) 89 | } 90 | }, 91 | 'ArrowFunctionExpression > CallExpression'(node) { 92 | checkCallExpression(node, node) 93 | }, 94 | } 95 | }, 96 | } 97 | -------------------------------------------------------------------------------- /docs/rules/no-callback-in-promise.md: -------------------------------------------------------------------------------- 1 | # Disallow calling `cb()` inside of a `then()` (use [util.callbackify][] instead) (`promise/no-callback-in-promise`) 2 | 3 | ⚠️ This rule _warns_ in the following configs: ✅ `flat/recommended`, ✅ 4 | `recommended`. 5 | 6 | 7 | 8 | As a general rule, callbacks should never be directly invoked inside a 9 | [Promise.prototype.then()] or [Promise.prototype.catch()] method. That's because 10 | your callback may be unintentionally be invoked twice. It also can be confusing 11 | to mix paradigms. 12 | 13 | Take the following example: 14 | 15 | ```js 16 | function callback(err, data) { 17 | console.log('Callback got called with:', err, data) 18 | throw new Error('My error') 19 | } 20 | 21 | // note: passing `err.message` for demo purposes, normally you would pass `err` 22 | Promise.resolve() 23 | .then(() => callback(null, 'data')) 24 | .catch((err) => callback(err.message, null)) 25 | ``` 26 | 27 | If you run this example, your output will look like the following: 28 | 29 | ``` 30 | Callback got called with: null data 31 | Callback got called with: My error null 32 | ``` 33 | 34 | ## Options 35 | 36 | ### `timeoutsErr` 37 | 38 | Boolean as to whether callbacks in timeout functions like `setTimeout` will err. 39 | Defaults to `false`. 40 | 41 | ## How to fix it? 42 | 43 | Ensure that your callback invocations are wrapped by a deferred execution 44 | function such as: 45 | 46 | - [setImmediate()] or [process.nextTick()]: for Node.js. 47 | - [setTimeout()]: for Browsers and Node.js. 48 | 49 | ```js 50 | // node.js 51 | Promise.resolve() 52 | .then(() => setImmediate(() => callback(null, 'data'))) 53 | .catch((err) => setImmediate(() => callback(err.message, null))) 54 | 55 | // node.js and browsers 56 | Promise.resolve() 57 | .then(() => setTimeout(() => callback(null, 'data'), 0)) 58 | .catch((err) => setTimeout(() => callback(err.message, null), 0)) 59 | ``` 60 | 61 | Your output will now look like the following: 62 | 63 | ```js 64 | Callback got called with: null data 65 | ``` 66 | 67 | Finally, if your callbacks have a Node.js signature (i.e. 68 | `callback(err, data)`), consider using [util.promisify] for promisifying your 69 | callback code instead of combining the approaches. 70 | 71 | [util.promisify]: 72 | https://nodejs.org/dist/latest/docs/api/util.html#utilpromisifyoriginal 73 | [promise.prototype.then()]: 74 | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then 75 | [promise.prototype.catch()]: 76 | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch 77 | [setimmediate()]: 78 | https://nodejs.org/docs/latest/api/timers.html#timers_setimmediate_callback_args 79 | [process.nexttick()]: 80 | https://nodejs.org/docs/latest/api/process.html#process_process_nexttick_callback_args 81 | [settimeout()]: 82 | https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout 83 | 84 | ## Options 85 | 86 | ### `exceptions` 87 | 88 | String list of callback function names to exempt. 89 | 90 | [util.callbackify]: 91 | https://nodejs.org/docs/latest/api/util.html#utilcallbackifyoriginal 92 | -------------------------------------------------------------------------------- /__tests__/no-promise-in-callback.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/no-promise-in-callback') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 6, 8 | }, 9 | }) 10 | 11 | const errorMessage = 'Avoid using promises inside of callbacks.' 12 | 13 | ruleTester.run('no-promise-in-callback', rule, { 14 | valid: [ 15 | 'go(function() { return Promise.resolve(4) })', 16 | 'go(function() { return a.then(b) })', 17 | 'go(function() { b.catch(c) })', 18 | 'go(function() { b.then(c, d) })', 19 | 20 | // arrow functions and other things 21 | 'go(() => Promise.resolve(4))', 22 | 'go((errrr) => a.then(b))', 23 | 'go((helpers) => { b.catch(c) })', 24 | 'go((e) => { b.then(c, d) })', 25 | 26 | // within promises it won't complain 27 | 'a.catch((err) => { b.then(c, d) })', 28 | 29 | // random unrelated things 30 | 'var x = function() { return Promise.resolve(4) }', 31 | 'function y() { return Promise.resolve(4) }', 32 | 'function then() { return Promise.reject() }', 33 | 'doThing(function(x) { return Promise.reject(x) })', 34 | 'doThing().then(function() { return Promise.all([a,b,c]) })', 35 | 'doThing().then(function() { return Promise.resolve(4) })', 36 | 'doThing().then(() => Promise.resolve(4))', 37 | 'doThing().then(() => Promise.all([a]))', 38 | 39 | // weird case, we assume it's not a big deal if you return (even though you may be cheating) 40 | 'a(function(err) { return doThing().then(a) })', 41 | 42 | { 43 | code: ` 44 | function fn(err) { 45 | return { promise: Promise.resolve(err) }; 46 | } 47 | `, 48 | options: [ 49 | { 50 | exemptDeclarations: true, 51 | }, 52 | ], 53 | }, 54 | ], 55 | 56 | invalid: [ 57 | { 58 | code: 'a(function(err) { doThing().then(a) })', 59 | errors: [{ message: errorMessage }], 60 | }, 61 | { 62 | code: 'a(function(error, zup, supa) { doThing().then(a) })', 63 | errors: [{ message: errorMessage }], 64 | }, 65 | { 66 | code: 'a(function(error) { doThing().then(a) })', 67 | errors: [{ message: errorMessage }], 68 | }, 69 | 70 | // arrow function 71 | { 72 | code: 'a((error) => { doThing().then(a) })', 73 | errors: [{ message: errorMessage }], 74 | }, 75 | { 76 | code: 'a((error) => doThing().then(a))', 77 | errors: [{ message: errorMessage }], 78 | }, 79 | { 80 | code: 'a((err, data) => { doThing().then(a) })', 81 | errors: [{ message: errorMessage }], 82 | }, 83 | { 84 | code: 'a((err, data) => doThing().then(a))', 85 | errors: [{ message: errorMessage }], 86 | }, 87 | 88 | // function decl. and similar (why not) 89 | { 90 | code: 'function x(err) { Promise.all() }', 91 | errors: [{ message: errorMessage }], 92 | }, 93 | { 94 | code: 'function x(err) { Promise.allSettled() }', 95 | errors: [{ message: errorMessage }], 96 | }, 97 | { 98 | code: 'function x(err) { Promise.any() }', 99 | errors: [{ message: errorMessage }], 100 | }, 101 | { 102 | code: 'let x = (err) => doThingWith(err).then(a)', 103 | errors: [{ message: errorMessage }], 104 | }, 105 | ], 106 | }) 107 | -------------------------------------------------------------------------------- /rules/no-callback-in-promise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rule: no-callback-in-promise 3 | * Avoid calling back inside of a promise 4 | */ 5 | 6 | 'use strict' 7 | 8 | const { getAncestors } = require('./lib/eslint-compat') 9 | const getDocsUrl = require('./lib/get-docs-url') 10 | const hasPromiseCallback = require('./lib/has-promise-callback') 11 | const isInsidePromise = require('./lib/is-inside-promise') 12 | const isCallback = require('./lib/is-callback') 13 | 14 | const CB_BLACKLIST = ['callback', 'cb', 'next', 'done'] 15 | const TIMEOUT_WHITELIST = [ 16 | 'setImmediate', 17 | 'setTimeout', 18 | 'requestAnimationFrame', 19 | 'nextTick', 20 | ] 21 | 22 | const isInsideTimeout = (node) => { 23 | const isFunctionExpression = 24 | node.type === 'FunctionExpression' || 25 | node.type === 'ArrowFunctionExpression' 26 | const parent = node.parent || {} 27 | const callee = parent.callee || {} 28 | const name = (callee.property && callee.property.name) || callee.name || '' 29 | const parentIsTimeout = TIMEOUT_WHITELIST.includes(name) 30 | const isInCB = isFunctionExpression && parentIsTimeout 31 | return isInCB 32 | } 33 | 34 | module.exports = { 35 | meta: { 36 | type: 'suggestion', 37 | docs: { 38 | description: 39 | 'Disallow calling `cb()` inside of a `then()` (use [util.callbackify][] instead).', 40 | url: getDocsUrl('no-callback-in-promise'), 41 | }, 42 | messages: { 43 | callback: 'Avoid calling back inside of a promise.', 44 | }, 45 | schema: [ 46 | { 47 | type: 'object', 48 | properties: { 49 | exceptions: { 50 | type: 'array', 51 | items: { 52 | type: 'string', 53 | }, 54 | }, 55 | timeoutsErr: { 56 | type: 'boolean', 57 | }, 58 | }, 59 | additionalProperties: false, 60 | }, 61 | ], 62 | }, 63 | create(context) { 64 | const { timeoutsErr = false } = context.options[0] || {} 65 | 66 | return { 67 | CallExpression(node) { 68 | const options = context.options[0] || {} 69 | const exceptions = options.exceptions || [] 70 | if (!isCallback(node, exceptions)) { 71 | const name = node.arguments?.[0]?.name 72 | if (hasPromiseCallback(node)) { 73 | const callingName = node.callee.name || node.callee.property?.name 74 | if ( 75 | !exceptions.includes(name) && 76 | CB_BLACKLIST.includes(name) && 77 | (timeoutsErr || !TIMEOUT_WHITELIST.includes(callingName)) 78 | ) { 79 | context.report({ 80 | node: node.arguments[0], 81 | messageId: 'callback', 82 | }) 83 | } 84 | return 85 | } 86 | if (!timeoutsErr) { 87 | return 88 | } 89 | 90 | if (!name) { 91 | // Will be handled elsewhere 92 | return 93 | } 94 | } 95 | 96 | const ancestors = getAncestors(context, node) 97 | if ( 98 | ancestors.some(isInsidePromise) && 99 | (timeoutsErr || !ancestors.some(isInsideTimeout)) 100 | ) { 101 | context.report({ 102 | node, 103 | messageId: 'callback', 104 | }) 105 | } 106 | }, 107 | } 108 | }, 109 | } 110 | -------------------------------------------------------------------------------- /__tests__/prefer-await-to-then.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/prefer-await-to-then') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 8, 8 | }, 9 | }) 10 | 11 | const message = 'Prefer await to then()/catch()/finally().' 12 | 13 | ruleTester.run('prefer-await-to-then', rule, { 14 | valid: [ 15 | 'async function hi() { await thing() }', 16 | 'async function hi() { await thing().then() }', 17 | 'async function hi() { await thing().catch() }', 18 | 'async function hi() { await thing().finally() }', 19 | 20 | // Cypress 21 | 'function hi() { cy.get(".myClass").then(go) }', 22 | 'function hi() { cy.get("button").click().then() }', 23 | 24 | 'function * hi() { yield thing().then() }', 25 | 'a = async () => (await something())', 26 | `a = async () => { 27 | try { await something() } catch (error) { somethingElse() } 28 | }`, 29 | 'something().then(async () => await somethingElse())', 30 | 'function foo() { hey.somethingElse(x => {}) }', 31 | `const isThenable = (obj) => { 32 | return obj && typeof obj.then === 'function'; 33 | };`, 34 | `function isThenable(obj) { 35 | return obj && typeof obj.then === 'function'; 36 | }`, 37 | `class Foo { 38 | constructor () { 39 | doSomething.then(abc); 40 | } 41 | }`, 42 | ], 43 | 44 | invalid: [ 45 | { 46 | code: 'function foo() { hey.then(x => {}) }', 47 | errors: [{ message }], 48 | }, 49 | { 50 | code: 'function foo() { hey.then(function() { }).then() }', 51 | errors: [{ message }, { message }], 52 | }, 53 | { 54 | code: 'function foo() { hey.then(function() { }).then(x).catch() }', 55 | errors: [{ message }, { message }, { message }], 56 | }, 57 | { 58 | code: 'async function a() { hey.then(function() { }).then(function() { }) }', 59 | errors: [{ message }, { message }], 60 | }, 61 | { 62 | code: 'function foo() { hey.catch(x => {}) }', 63 | errors: [{ message }], 64 | }, 65 | { 66 | code: 'function foo() { hey.finally(x => {}) }', 67 | errors: [{ message }], 68 | }, 69 | { 70 | code: 'async function hi() { await thing().then() }', 71 | errors: [{ message }], 72 | options: [ 73 | { 74 | strict: true, 75 | }, 76 | ], 77 | }, 78 | { 79 | code: `class Foo { 80 | constructor () { 81 | doSomething.then(abc); 82 | } 83 | }`, 84 | errors: [{ message }], 85 | options: [ 86 | { 87 | strict: true, 88 | }, 89 | ], 90 | }, 91 | { 92 | code: 'async function hi() { await thing().catch() }', 93 | errors: [{ message }], 94 | options: [ 95 | { 96 | strict: true, 97 | }, 98 | ], 99 | }, 100 | { 101 | code: 'async function hi() { await thing().finally() }', 102 | errors: [{ message }], 103 | options: [ 104 | { 105 | strict: true, 106 | }, 107 | ], 108 | }, 109 | { 110 | code: 'function * hi() { yield thing().then() }', 111 | errors: [{ message }], 112 | options: [ 113 | { 114 | strict: true, 115 | }, 116 | ], 117 | }, 118 | ], 119 | }) 120 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at macklinu@gmail.com. The project team 59 | will review and investigate all complaints, and will respond in a way that it 60 | deems appropriate to the circumstances. The project team is obligated to 61 | maintain confidentiality with regard to the reporter of an incident. Further 62 | details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 71 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /rules/prefer-await-to-callbacks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { getAncestors } = require('./lib/eslint-compat') 4 | const getDocsUrl = require('./lib/get-docs-url') 5 | 6 | module.exports = { 7 | meta: { 8 | type: 'suggestion', 9 | docs: { 10 | description: 'Prefer `async`/`await` to the callback pattern.', 11 | url: getDocsUrl('prefer-await-to-callbacks'), 12 | }, 13 | messages: { 14 | error: 'Avoid callbacks. Prefer Async/Await.', 15 | }, 16 | schema: [], 17 | }, 18 | create(context) { 19 | function checkLastParamsForCallback(node) { 20 | const lastParam = node.params[node.params.length - 1] || {} 21 | if (lastParam.name === 'callback' || lastParam.name === 'cb') { 22 | context.report({ node: lastParam, messageId: 'error' }) 23 | } 24 | } 25 | function isInsideYieldOrAwait(node) { 26 | return getAncestors(context, node).some((parent) => { 27 | return ( 28 | parent.type === 'AwaitExpression' || parent.type === 'YieldExpression' 29 | ) 30 | }) 31 | } 32 | return { 33 | CallExpression(node) { 34 | // Callbacks aren't allowed. 35 | if (node.callee.name === 'cb' || node.callee.name === 'callback') { 36 | context.report({ node, messageId: 'error' }) 37 | return 38 | } 39 | 40 | // Then-ables aren't allowed either. 41 | const args = node.arguments 42 | const lastArgIndex = args.length - 1 43 | const arg = lastArgIndex > -1 && node.arguments[lastArgIndex] 44 | if ( 45 | (arg && arg.type === 'FunctionExpression') || 46 | arg.type === 'ArrowFunctionExpression' 47 | ) { 48 | // Ignore event listener callbacks. 49 | if ( 50 | node.callee.property && 51 | (node.callee.property.name === 'on' || 52 | node.callee.property.name === 'once') 53 | ) { 54 | return 55 | } 56 | 57 | // carve out exemption for map/filter/etc 58 | const arrayMethods = [ 59 | 'map', 60 | 'every', 61 | 'forEach', 62 | 'some', 63 | 'find', 64 | 'filter', 65 | ] 66 | const isLodash = 67 | node.callee.object && 68 | ['lodash', 'underscore', '_'].includes(node.callee.object.name) 69 | const callsArrayMethod = 70 | node.callee.property && 71 | arrayMethods.includes(node.callee.property.name) && 72 | (node.arguments.length === 1 || 73 | (node.arguments.length === 2 && isLodash)) 74 | const isArrayMethod = 75 | node.callee.name && 76 | arrayMethods.includes(node.callee.name) && 77 | node.arguments.length === 2 78 | if (callsArrayMethod || isArrayMethod) return 79 | 80 | // actually check for callbacks (I know this is the worst) 81 | if ( 82 | arg.params && 83 | arg.params[0] && 84 | (arg.params[0].name === 'err' || arg.params[0].name === 'error') 85 | ) { 86 | if (!isInsideYieldOrAwait(node)) { 87 | context.report({ node: arg, messageId: 'error' }) 88 | } 89 | } 90 | } 91 | }, 92 | FunctionDeclaration: checkLastParamsForCallback, 93 | FunctionExpression: checkLastParamsForCallback, 94 | ArrowFunctionExpression: checkLastParamsForCallback, 95 | } 96 | }, 97 | } 98 | -------------------------------------------------------------------------------- /__tests__/no-nesting.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/no-nesting') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 6, 8 | }, 9 | }) 10 | 11 | const errorMessage = 'Avoid nesting promises.' 12 | 13 | ruleTester.run('no-nesting', rule, { 14 | valid: [ 15 | // resolve and reject are sometimes okay 16 | 'Promise.resolve(4).then(function(x) { return x })', 17 | 'Promise.reject(4).then(function(x) { return x })', 18 | 'Promise.resolve(4).then(function() {})', 19 | 'Promise.reject(4).then(function() {})', 20 | 21 | // throw and return are fine 22 | 'doThing().then(function() { return 4 })', 23 | 'doThing().then(function() { throw 4 })', 24 | 'doThing().then(null, function() { return 4 })', 25 | 'doThing().then(null, function() { throw 4 })', 26 | 'doThing().catch(null, function() { return 4 })', 27 | 'doThing().catch(null, function() { throw 4 })', 28 | 29 | // arrow functions and other things 30 | 'doThing().then(() => 4)', 31 | 'doThing().then(() => { throw 4 })', 32 | 'doThing().then(()=>{}, () => 4)', 33 | 'doThing().then(()=>{}, () => { throw 4 })', 34 | 'doThing().catch(() => 4)', 35 | 'doThing().catch(() => { throw 4 })', 36 | 37 | // random functions and callback methods 38 | 'var x = function() { return Promise.resolve(4) }', 39 | 'function y() { return Promise.resolve(4) }', 40 | 'function then() { return Promise.reject() }', 41 | 'doThing(function(x) { return Promise.reject(x) })', 42 | 43 | // this should be allowed basically, we're mostly raging against using then() 44 | 'doThing().then(function() { return Promise.all([a,b,c]) })', 45 | 'doThing().then(function() { return Promise.resolve(4) })', 46 | 'doThing().then(() => Promise.resolve(4))', 47 | 'doThing().then(() => Promise.all([a]))', 48 | 49 | // references vars in closure 50 | `doThing() 51 | .then(a => getB(a) 52 | .then(b => getC(a, b)) 53 | )`, 54 | `doThing() 55 | .then(a => { 56 | const c = a * 2; 57 | return getB(c).then(b => getC(c, b)) 58 | })`, 59 | ], 60 | 61 | invalid: [ 62 | { 63 | code: 'doThing().then(function() { a.then() })', 64 | errors: [{ message: errorMessage }], 65 | }, 66 | { 67 | code: 'doThing().then(function() { b.catch() })', 68 | errors: [{ message: errorMessage }], 69 | }, 70 | { 71 | code: 'doThing().then(function() { return a.then() })', 72 | errors: [{ message: errorMessage }], 73 | }, 74 | { 75 | code: 'doThing().then(function() { return b.catch() })', 76 | errors: [{ message: errorMessage }], 77 | }, 78 | { 79 | code: 'doThing().then(() => { a.then() })', 80 | errors: [{ message: errorMessage }], 81 | }, 82 | { 83 | code: 'doThing().then(() => { b.catch() })', 84 | errors: [{ message: errorMessage }], 85 | }, 86 | { 87 | code: 'doThing().then(() => a.then())', 88 | errors: [{ message: errorMessage }], 89 | }, 90 | { 91 | code: 'doThing().then(() => b.catch())', 92 | errors: [{ message: errorMessage }], 93 | }, 94 | // references vars in closure 95 | { 96 | code: ` 97 | doThing() 98 | .then(a => getB(a) 99 | .then(b => getC(b)) 100 | )`, 101 | errors: [{ message: errorMessage, line: 4 }], 102 | }, 103 | { 104 | code: ` 105 | doThing() 106 | .then(a => getB(a) 107 | .then(b => getC(a, b) 108 | .then(c => getD(a, c)) 109 | ) 110 | )`, 111 | errors: [{ message: errorMessage, line: 5 }], 112 | }, 113 | ], 114 | }) 115 | -------------------------------------------------------------------------------- /docs/rules/catch-or-return.md: -------------------------------------------------------------------------------- 1 | # Enforce the use of `catch()` on un-returned promises (`promise/catch-or-return`) 2 | 3 | 💼 This rule is enabled in the following configs: ✅ `flat/recommended`, ✅ 4 | `recommended`. 5 | 6 | 7 | 8 | Ensure that each time a `then()` is applied to a promise, a `catch()` is applied 9 | as well. Exceptions are made if you are returning that promise. 10 | 11 | #### Valid 12 | 13 | ```js 14 | myPromise.then(doSomething).catch(errors) 15 | myPromise.then(doSomething).then(doSomethingElse).catch(errors) 16 | function doSomethingElse() { 17 | return myPromise.then(doSomething) 18 | } 19 | ``` 20 | 21 | #### Invalid 22 | 23 | ```js 24 | myPromise.then(doSomething) 25 | myPromise.then(doSomething, catchErrors) // catch() may be a little better 26 | function doSomethingElse() { 27 | return myPromise.then(doSomething) 28 | } 29 | ``` 30 | 31 | #### Options 32 | 33 | ##### `allowThen` 34 | 35 | The second argument to `then()` can also be used to handle a promise rejection, 36 | but it won't catch any errors from the first argument callback. Because of this, 37 | this rule reports usage of `then()` with two arguments without `catch()` by 38 | default. 39 | 40 | However, you can use `{ allowThen: true }` to allow using `then()` with two 41 | arguments instead of `catch()` to handle promise rejections. 42 | 43 | Examples of **incorrect** code for the default `{ allowThen: false }` option: 44 | 45 | ```js 46 | myPromise.then(doSomething, handleErrors) 47 | myPromise.then(null, handleErrors) 48 | ``` 49 | 50 | Examples of **correct** code for the `{ allowThen: true }` option: 51 | 52 | ```js 53 | myPromise.then(doSomething, handleErrors) 54 | myPromise.then(null, handleErrors) 55 | myPromise.then(doSomething).catch(handleErrors) 56 | ``` 57 | 58 | ##### `allowThenStrict` 59 | 60 | `allowThenStrict` is similar to `allowThen` but it only permits `then` when the 61 | fulfillment handler is `null`. This option ensures that the final rejected 62 | handler works like a `catch` and can handle any uncaught errors from the final 63 | `then`. 64 | 65 | Examples of **incorrect** code for the default `{ allowThenStrict: false }` 66 | option: 67 | 68 | ```js 69 | myPromise.then(doSomething, handleErrors) 70 | myPromise.then(null, handleErrors) 71 | ``` 72 | 73 | Examples of **correct** code for the `{ allowThenStrict: true }` option: 74 | 75 | ```js 76 | myPromise.then(null, handleErrors) 77 | myPromise.then(doSomething).catch(handleErrors) 78 | ``` 79 | 80 | Examples of **incorrect** code for the `{ allowThenStrict: true }` option: 81 | 82 | ```js 83 | myPromise.then(doSomething, handleErrors) 84 | ``` 85 | 86 | ##### `allowFinally` 87 | 88 | You can pass an `{ allowFinally: true }` as an option to this rule to allow for 89 | `.finally(fn)` to be used after `catch()` at the end of the promise chain. This 90 | is different from adding `'finally'` as a `terminationMethod` because it will 91 | still require the Promise chain to be "caught" beforehand. 92 | 93 | ##### `terminationMethod` 94 | 95 | You can pass a `{ terminationMethod: 'done' }` as an option to this rule to 96 | require `done()` instead of `catch()` at the end of the promise chain. This is 97 | useful for many non-standard Promise implementations. 98 | 99 | You can also pass an array of methods such as 100 | `{ terminationMethod: ['catch', 'asCallback', 'finally'] }`. 101 | 102 | This will allow any of 103 | 104 | ```js 105 | Promise.resolve(1) 106 | .then(() => { 107 | throw new Error('oops') 108 | }) 109 | .catch(logerror) 110 | Promise.resolve(1) 111 | .then(() => { 112 | throw new Error('oops') 113 | }) 114 | .asCallback(cb) 115 | Promise.resolve(1) 116 | .then(() => { 117 | throw new Error('oops') 118 | }) 119 | .finally(cleanUp) 120 | ``` 121 | -------------------------------------------------------------------------------- /rules/no-nesting.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rule: no-nesting 3 | * Avoid nesting your promises. 4 | */ 5 | 6 | 'use strict' 7 | 8 | const { getScope } = require('./lib/eslint-compat') 9 | const getDocsUrl = require('./lib/get-docs-url') 10 | const hasPromiseCallback = require('./lib/has-promise-callback') 11 | const isInsidePromise = require('./lib/is-inside-promise') 12 | 13 | module.exports = { 14 | meta: { 15 | type: 'suggestion', 16 | docs: { 17 | description: 'Disallow nested `then()` or `catch()` statements.', 18 | url: getDocsUrl('no-nesting'), 19 | }, 20 | schema: [], 21 | messages: { 22 | avoidNesting: 'Avoid nesting promises.', 23 | }, 24 | }, 25 | create(context) { 26 | /** 27 | * Array of callback function scopes. 28 | * Scopes are in order closest to the current node. 29 | * @type {import('eslint').Scope.Scope[]} 30 | */ 31 | const callbackScopes = [] 32 | 33 | /** 34 | * @param {import('eslint').Scope.Scope} scope 35 | * @returns {Iterable} 36 | */ 37 | function* iterateDefinedReferences(scope) { 38 | for (const variable of scope.variables) { 39 | for (const reference of variable.references) { 40 | yield reference 41 | } 42 | } 43 | } 44 | 45 | return { 46 | ':function'(node) { 47 | if (isInsidePromise(node)) { 48 | callbackScopes.unshift(getScope(context, node)) 49 | } 50 | }, 51 | ':function:exit'(node) { 52 | if (isInsidePromise(node)) { 53 | callbackScopes.shift() 54 | } 55 | }, 56 | CallExpression(node) { 57 | if (!hasPromiseCallback(node)) return 58 | if (!callbackScopes.length) { 59 | // The node is not in the callback function. 60 | return 61 | } 62 | 63 | // Checks if the argument callback uses variables defined in the closest callback function scope. 64 | // 65 | // e.g. 66 | // ``` 67 | // doThing() 68 | // .then(a => getB(a) 69 | // .then(b => getC(a, b)) 70 | // ) 71 | // ``` 72 | // 73 | // In the above case, Since the variables it references are undef, 74 | // we cannot refactor the nesting like following: 75 | // ``` 76 | // doThing() 77 | // .then(a => getB(a)) 78 | // .then(b => getC(a, b)) 79 | // ``` 80 | // 81 | // However, `getD` can be refactored in the following: 82 | // ``` 83 | // doThing() 84 | // .then(a => getB(a) 85 | // .then(b => getC(a, b) 86 | // .then(c => getD(a, c)) 87 | // ) 88 | // ) 89 | // ``` 90 | // ↓ 91 | // ``` 92 | // doThing() 93 | // .then(a => getB(a) 94 | // .then(b => getC(a, b)) 95 | // .then(c => getD(a, c)) 96 | // ) 97 | // ``` 98 | // This is why we only check the closest callback function scope. 99 | // 100 | const closestCallbackScope = callbackScopes[0] 101 | for (const reference of iterateDefinedReferences( 102 | closestCallbackScope, 103 | )) { 104 | if ( 105 | node.arguments.some( 106 | (arg) => 107 | arg.range[0] <= reference.identifier.range[0] && 108 | reference.identifier.range[1] <= arg.range[1], 109 | ) 110 | ) { 111 | // Argument callbacks refer to variables defined in the callback function. 112 | return 113 | } 114 | } 115 | 116 | context.report({ 117 | node: node.callee.property, 118 | messageId: 'avoidNesting', 119 | }) 120 | }, 121 | } 122 | }, 123 | } 124 | -------------------------------------------------------------------------------- /rules/catch-or-return.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rule: catch-or-return 3 | * Ensures that promises either include a catch() handler 4 | * or are returned (to be handled upstream) 5 | */ 6 | 7 | 'use strict' 8 | 9 | const getDocsUrl = require('./lib/get-docs-url') 10 | const isPromise = require('./lib/is-promise') 11 | const isMemberCallWithObjectName = require('./lib/is-member-call-with-object-name') 12 | 13 | module.exports = { 14 | meta: { 15 | type: 'problem', 16 | docs: { 17 | description: 'Enforce the use of `catch()` on un-returned promises.', 18 | url: getDocsUrl('catch-or-return'), 19 | }, 20 | messages: { 21 | terminationMethod: 'Expected {{ terminationMethod }}() or return', 22 | }, 23 | schema: [ 24 | { 25 | type: 'object', 26 | properties: { 27 | allowFinally: { 28 | type: 'boolean', 29 | }, 30 | allowThen: { 31 | type: 'boolean', 32 | }, 33 | allowThenStrict: { 34 | type: 'boolean', 35 | }, 36 | terminationMethod: { 37 | oneOf: [ 38 | { type: 'string' }, 39 | { 40 | type: 'array', 41 | items: { 42 | type: 'string', 43 | }, 44 | }, 45 | ], 46 | }, 47 | }, 48 | additionalProperties: false, 49 | }, 50 | ], 51 | }, 52 | create(context) { 53 | const options = context.options[0] || {} 54 | const allowThen = options.allowThen 55 | const allowThenStrict = options.allowThenStrict 56 | const allowFinally = options.allowFinally 57 | let terminationMethod = options.terminationMethod || 'catch' 58 | 59 | if (typeof terminationMethod === 'string') { 60 | terminationMethod = [terminationMethod] 61 | } 62 | 63 | function isAllowedPromiseTermination(expression) { 64 | // somePromise.then(a, b) 65 | if ( 66 | (allowThen || allowThenStrict) && 67 | expression.type === 'CallExpression' && 68 | expression.callee.type === 'MemberExpression' && 69 | expression.callee.property.name === 'then' && 70 | expression.arguments.length === 2 && 71 | // somePromise.then(null, b) 72 | ((allowThen && !allowThenStrict) || 73 | (expression.arguments[0].type === 'Literal' && 74 | expression.arguments[0].value === null)) 75 | ) { 76 | return true 77 | } 78 | 79 | // somePromise.catch().finally(fn) 80 | if ( 81 | allowFinally && 82 | expression.type === 'CallExpression' && 83 | expression.callee.type === 'MemberExpression' && 84 | expression.callee.property.name === 'finally' && 85 | isPromise(expression.callee.object) && 86 | isAllowedPromiseTermination(expression.callee.object) 87 | ) { 88 | return true 89 | } 90 | 91 | // somePromise.catch() 92 | if ( 93 | expression.type === 'CallExpression' && 94 | expression.callee.type === 'MemberExpression' && 95 | terminationMethod.indexOf(expression.callee.property.name) !== -1 96 | ) { 97 | return true 98 | } 99 | 100 | // somePromise['catch']() 101 | if ( 102 | expression.type === 'CallExpression' && 103 | expression.callee.type === 'MemberExpression' && 104 | expression.callee.property.type === 'Literal' && 105 | expression.callee.property.value === 'catch' 106 | ) { 107 | return true 108 | } 109 | 110 | // cy.get().then(a, b); 111 | if (isMemberCallWithObjectName('cy', expression)) { 112 | return true 113 | } 114 | 115 | return false 116 | } 117 | 118 | return { 119 | ExpressionStatement(node) { 120 | if (!isPromise(node.expression)) { 121 | return 122 | } 123 | 124 | if (isAllowedPromiseTermination(node.expression)) { 125 | return 126 | } 127 | 128 | context.report({ 129 | node, 130 | messageId: 'terminationMethod', 131 | data: { terminationMethod }, 132 | }) 133 | }, 134 | } 135 | }, 136 | } 137 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to eslint-plugin-promise 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | Please note that this project is released with a 6 | [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this 7 | project, you agree to abide by its terms. 8 | 9 | ## Table of Contents 10 | 11 | 12 | 13 | 14 | - [How can I contribute?](#how-can-i-contribute) 15 | - [Improve documentation](#improve-documentation) 16 | - [Improve issues](#improve-issues) 17 | - [Give feedback on issues](#give-feedback-on-issues) 18 | - [Write code](#write-code) 19 | - [Setup](#setup) 20 | - [Submitting an issue](#submitting-an-issue) 21 | 22 | 23 | 24 | ## How can I contribute? 25 | 26 | ### Improve documentation 27 | 28 | As a user of eslint-plugin-promise, you're the perfect candidate to help us 29 | improve our documentation. Typo corrections, error fixes, better explanations, 30 | more examples, etc. Open issues for things that could be improved. Anything. 31 | Even improvements to this document. 32 | 33 | ### Improve issues 34 | 35 | Some issues are created with missing information, not reproducible, or plain 36 | invalid. Help make them easier to resolve. 37 | 38 | ### Give feedback on issues 39 | 40 | We're always looking for more opinions on discussions in the issue tracker. It's 41 | a good opportunity to influence the future direction of this tool. 42 | 43 | The 44 | [`question` label](https://github.com/eslint-community/eslint-plugin-promise/labels/question) 45 | is a good place to find ongoing discussions. 46 | 47 | ### Write code 48 | 49 | You can use issue labels to discover issues you could help out with: 50 | 51 | - [`bug` issues](https://github.com/eslint-community/eslint-plugin-promise/labels/bug) 52 | are known bugs we'd like to fix 53 | - [`enhancement` issues](https://github.com/eslint-community/eslint-plugin-promise/labels/enhancement) 54 | are features we're open to including 55 | 56 | The 57 | [`help wanted`](https://github.com/eslint-community/eslint-plugin-promise/labels/help%20wanted) 58 | and 59 | [`good first issue`](https://github.com/eslint-community/eslint-plugin-promise/labels/good%20first%20issue) 60 | labels are especially useful. 61 | 62 | You may find an issue is assigned. Please double-check before starting on this 63 | issue because somebody else is likely already working on it. 64 | 65 | ## Setup 66 | 67 | When developing, prefer using Node ≥8 and npm ≥5. While this plugin supports 68 | Node 4, writing code with the latest stable Node and npm versions allows us to 69 | use newer developer tools. 70 | 71 | After 72 | [cloning the repository](https://help.github.com/articles/cloning-a-repository/), 73 | run `npm install` to install dependencies. 74 | 75 | Run `npm test` to run the test suite (powered by 76 | [Jest](https://facebook.github.io/jest/)). Sometimes it can be helpful to run a 77 | single test file and watch for changes, especially when working on a single 78 | rule. You can run `npm test -- --watch` to achieve this. 79 | 80 | Run `npm run lint` to lint the codebase. If there are any errors reported, try 81 | running `npm run lint -- --fix` first to see if ESLint can fix them for you. 82 | `npm test` will also lint the codebase thanks to 83 | [jest-runner-eslint](https://github.com/jest-community/jest-runner-eslint). 84 | 85 | This codebase uses [Prettier](http://prettier.io/) for code formatting. Consider 86 | installing an [editor plugin](https://prettier.io/docs/en/editors.html) for the 87 | best experience, but code will also be formatted with a precommit script (using 88 | [lint-staged](https://github.com/okonet/lint-staged)) as well as by running 89 | `npm run lint -- --fix`. 90 | 91 | ## Submitting an issue 92 | 93 | - Please search the issue tracker before opening an issue. 94 | - Use a clear and descriptive title. 95 | - Include as much information as possible by filling out the provided issue 96 | template. 97 | - The more time you put into an issue, the more we will. 98 | - [The best issue report is a failing test proving it.](https://twitter.com/sindresorhus/status/579306280495357953) 99 | -------------------------------------------------------------------------------- /__tests__/no-callback-in-promise.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/no-callback-in-promise') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 6, 8 | }, 9 | }) 10 | 11 | const errorMessage = 'Avoid calling back inside of a promise.' 12 | 13 | ruleTester.run('no-callback-in-promise', rule, { 14 | valid: [ 15 | 'function thing(cb) { cb() }', 16 | 'doSomething(function(err) { cb(err) })', 17 | 'function thing(callback) { callback() }', 18 | 'doSomething(function(err) { callback(err) })', 19 | 20 | // Support safe callbacks (#220) 21 | 'whatever.then((err) => { process.nextTick(() => cb()) })', 22 | 'whatever.then((err) => { setImmediate(() => cb()) })', 23 | 'whatever.then((err) => setImmediate(() => cb()))', 24 | 'whatever.then((err) => process.nextTick(() => cb()))', 25 | 'whatever.then((err) => process.nextTick(cb))', 26 | 'whatever.then((err) => setImmediate(cb))', 27 | 28 | // arrow functions and other things 29 | 'let thing = (cb) => cb()', 30 | 'doSomething(err => cb(err))', 31 | 32 | // exceptions test 33 | { 34 | code: 'a.then(() => next())', 35 | options: [{ exceptions: ['next'] }], 36 | }, 37 | { 38 | code: 'a.then(() => next()).catch((err) => next(err))', 39 | options: [{ exceptions: ['next'] }], 40 | }, 41 | { 42 | code: 'a.then(next)', 43 | options: [{ exceptions: ['next'] }], 44 | }, 45 | { 46 | code: 'a.then(next).catch(next)', 47 | options: [{ exceptions: ['next'] }], 48 | }, 49 | 50 | // #572 51 | `while (!(step = call(next, iterator)).done) { 52 | if (result !== undefined) break; 53 | }`, 54 | // https://github.com/eslint-community/eslint-plugin-promise/issues/572#issuecomment-2501505747 55 | `function hasCallbackArg(callback) { 56 | console.log(callback); 57 | }`, 58 | ], 59 | 60 | invalid: [ 61 | { 62 | code: 'a.then(cb)', 63 | errors: [{ message: errorMessage, column: 8 }], 64 | }, 65 | { 66 | code: 'a.then(() => cb())', 67 | errors: [{ message: errorMessage }], 68 | }, 69 | { 70 | code: 'a.then(function(err) { cb(err) })', 71 | errors: [{ message: errorMessage, column: 24 }], 72 | }, 73 | { 74 | code: 'a.then(function(data) { cb(data) }, function(err) { cb(err) })', 75 | errors: [ 76 | { column: 25, message: errorMessage }, 77 | { column: 53, message: errorMessage }, 78 | ], 79 | }, 80 | { 81 | code: 'a.catch(function(err) { cb(err) })', 82 | errors: [{ message: errorMessage }], 83 | }, 84 | 85 | // callback should also complain 86 | { 87 | code: 'a.then(callback)', 88 | errors: [{ message: errorMessage, column: 8 }], 89 | }, 90 | { 91 | code: 'a.then(() => callback())', 92 | errors: [{ message: errorMessage }], 93 | }, 94 | { 95 | code: 'a.then(function(err) { callback(err) })', 96 | errors: [{ message: errorMessage, column: 24 }], 97 | }, 98 | { 99 | code: 'a.then(function(data) { callback(data) }, function(err) { callback(err) })', 100 | errors: [ 101 | { message: errorMessage }, 102 | { column: 59, message: errorMessage }, 103 | ], 104 | }, 105 | { 106 | code: 'a.catch(function(err) { callback(err) })', 107 | errors: [{ message: errorMessage }], 108 | }, 109 | 110 | // #167 111 | { 112 | code: ` 113 | function wait (callback) { 114 | return Promise.resolve() 115 | .then(() => { 116 | setTimeout(callback); 117 | }); 118 | } 119 | `, 120 | errors: [{ message: errorMessage }], 121 | options: [ 122 | { 123 | timeoutsErr: true, 124 | }, 125 | ], 126 | }, 127 | { 128 | code: ` 129 | function wait (callback) { 130 | return Promise.resolve() 131 | .then(() => { 132 | setTimeout(() => callback()); 133 | }); 134 | } 135 | `, 136 | errors: [{ message: errorMessage }], 137 | options: [ 138 | { 139 | timeoutsErr: true, 140 | }, 141 | ], 142 | }, 143 | 144 | { 145 | code: 'whatever.then((err) => { process.nextTick(() => cb()) })', 146 | errors: [{ message: errorMessage }], 147 | options: [ 148 | { 149 | timeoutsErr: true, 150 | }, 151 | ], 152 | }, 153 | { 154 | code: 'whatever.then((err) => { setImmediate(() => cb()) })', 155 | errors: [{ message: errorMessage }], 156 | options: [ 157 | { 158 | timeoutsErr: true, 159 | }, 160 | ], 161 | }, 162 | { 163 | code: 'whatever.then((err) => setImmediate(() => cb()))', 164 | errors: [{ message: errorMessage }], 165 | options: [ 166 | { 167 | timeoutsErr: true, 168 | }, 169 | ], 170 | }, 171 | { 172 | code: 'whatever.then((err) => process.nextTick(() => cb()))', 173 | errors: [{ message: errorMessage }], 174 | options: [ 175 | { 176 | timeoutsErr: true, 177 | }, 178 | ], 179 | }, 180 | { 181 | code: 'whatever.then((err) => process.nextTick(cb))', 182 | errors: [{ message: errorMessage }], 183 | options: [ 184 | { 185 | timeoutsErr: true, 186 | }, 187 | ], 188 | }, 189 | { 190 | code: 'whatever.then((err) => setImmediate(cb))', 191 | errors: [{ message: errorMessage }], 192 | options: [ 193 | { 194 | timeoutsErr: true, 195 | }, 196 | ], 197 | }, 198 | ], 199 | }) 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-promise 2 | 3 | Enforce best practices for JavaScript promises. 4 | 5 | [![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) 6 | [![npm version](https://badge.fury.io/js/eslint-plugin-promise.svg)](https://www.npmjs.com/package/eslint-plugin-promise) 7 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 8 | 9 | 10 | 11 | 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | - [Rules](#rules) 15 | - [Maintainers](#maintainers) 16 | - [License](#license) 17 | 18 | 19 | 20 | ## Installation 21 | 22 | You'll first need to install [ESLint](http://eslint.org): 23 | 24 | ```sh 25 | npm install eslint --save-dev 26 | ``` 27 | 28 | Next, install `eslint-plugin-promise`: 29 | 30 | ```sh 31 | npm install eslint-plugin-promise --save-dev 32 | ``` 33 | 34 | **Note:** If you installed ESLint globally (using the `-g` flag) then you must 35 | also install `eslint-plugin-promise` globally. 36 | 37 | ## Usage 38 | 39 | Add `promise` to the plugins section of your `.eslintrc.json` configuration 40 | file. You can omit the `eslint-plugin-` prefix: 41 | 42 | ```json 43 | { 44 | "plugins": ["promise"] 45 | } 46 | ``` 47 | 48 | Then configure the rules you want to use under the rules section. 49 | 50 | ```json 51 | { 52 | "rules": { 53 | "promise/always-return": "error", 54 | "promise/no-return-wrap": "error", 55 | "promise/param-names": "error", 56 | "promise/catch-or-return": "error", 57 | "promise/no-native": "off", 58 | "promise/no-nesting": "warn", 59 | "promise/no-promise-in-callback": "warn", 60 | "promise/no-callback-in-promise": "warn", 61 | "promise/avoid-new": "warn", 62 | "promise/no-new-statics": "error", 63 | "promise/no-return-in-finally": "warn", 64 | "promise/valid-params": "warn", 65 | "promise/no-multiple-resolved": "error" 66 | } 67 | } 68 | ``` 69 | 70 | or start with the recommended rule set: 71 | 72 | - `eslint.config.js`: 73 | 74 | ```js 75 | import pluginPromise from 'eslint-plugin-promise' 76 | export default [ 77 | // ... 78 | pluginPromise.configs['flat/recommended'], 79 | ] 80 | ``` 81 | 82 | - `.eslintrc.*`: 83 | 84 | ```json 85 | { 86 | "extends": ["plugin:promise/recommended"] 87 | } 88 | ``` 89 | 90 | ## Rules 91 | 92 | 93 | 94 | 💼 Configurations enabled in.\ 95 | ⚠️ Configurations set to warn in.\ 96 | 🚫 Configurations disabled in.\ 97 | ✅ Set in the `flat/recommended` configuration.\ 98 | ✅ Set in the `recommended` configuration.\ 99 | 🔧 Automatically fixable by the 100 | [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). 101 | 102 | | Name                      | Description | 💼 | ⚠️ | 🚫 | 🔧 | 103 | | :------------------------------------------------------------------- | :----------------------------------------------------------------------------------------- | :-- | :-- | :-- | :-- | 104 | | [always-return](docs/rules/always-return.md) | Require returning inside each `then()` to create readable and reusable Promise chains. | ✅ | | | | 105 | | [avoid-new](docs/rules/avoid-new.md) | Disallow creating `new` promises outside of utility libs (use [util.promisify][] instead). | | | ✅ | | 106 | | [catch-or-return](docs/rules/catch-or-return.md) | Enforce the use of `catch()` on un-returned promises. | ✅ | | | | 107 | | [no-callback-in-promise](docs/rules/no-callback-in-promise.md) | Disallow calling `cb()` inside of a `then()` (use [util.callbackify][] instead). | | ✅ | | | 108 | | [no-multiple-resolved](docs/rules/no-multiple-resolved.md) | Disallow creating new promises with paths that resolve multiple times. | | | | | 109 | | [no-native](docs/rules/no-native.md) | Require creating a `Promise` constructor before using it in an ES5 environment. | | | ✅ | | 110 | | [no-nesting](docs/rules/no-nesting.md) | Disallow nested `then()` or `catch()` statements. | | ✅ | | | 111 | | [no-new-statics](docs/rules/no-new-statics.md) | Disallow calling `new` on a Promise static method. | ✅ | | | 🔧 | 112 | | [no-promise-in-callback](docs/rules/no-promise-in-callback.md) | Disallow using promises inside of callbacks. | | ✅ | | | 113 | | [no-return-in-finally](docs/rules/no-return-in-finally.md) | Disallow return statements in `finally()`. | | ✅ | | | 114 | | [no-return-wrap](docs/rules/no-return-wrap.md) | Disallow wrapping values in `Promise.resolve` or `Promise.reject` when not needed. | ✅ | | | | 115 | | [param-names](docs/rules/param-names.md) | Enforce consistent param names and ordering when creating new promises. | ✅ | | | | 116 | | [prefer-await-to-callbacks](docs/rules/prefer-await-to-callbacks.md) | Prefer `async`/`await` to the callback pattern. | | | | | 117 | | [prefer-await-to-then](docs/rules/prefer-await-to-then.md) | Prefer `await` to `then()`/`catch()`/`finally()` for reading Promise values. | | | | | 118 | | [prefer-catch](docs/rules/prefer-catch.md) | Prefer `catch` to `then(a, b)`/`then(null, b)` for handling errors. | | | | 🔧 | 119 | | [spec-only](docs/rules/spec-only.md) | Disallow use of non-standard Promise static methods. | | | | | 120 | | [valid-params](docs/rules/valid-params.md) | Enforces the proper number of arguments are passed to Promise functions. | | ✅ | | | 121 | 122 | 123 | 124 | ## Maintainers 125 | 126 | - Jamund Ferguson - [@xjamundx][] 127 | - Macklin Underdown - [@macklinu][] 128 | - Aadit M Shah - [@aaditmshah][] 129 | 130 | ## License 131 | 132 | - (c) MMXV jden - ISC license. 133 | - (c) 2016 Jamund Ferguson - ISC license. 134 | 135 | [util.callbackify]: 136 | https://nodejs.org/docs/latest/api/util.html#utilcallbackifyoriginal 137 | [util.promisify]: https://nodejs.org/api/util.html#util_util_promisify_original 138 | [@aaditmshah]: https://github.com/aaditmshah 139 | [@macklinu]: https://github.com/macklinu 140 | [@xjamundx]: https://github.com/xjamundx 141 | -------------------------------------------------------------------------------- /__tests__/valid-params.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/valid-params') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 6, 8 | }, 9 | }) 10 | 11 | ruleTester.run('valid-params', rule, { 12 | valid: [ 13 | // valid Promise.resolve() 14 | 'Promise.resolve()', 15 | 'Promise.resolve(1)', 16 | 'Promise.resolve({})', 17 | 'Promise.resolve(referenceToSomething)', 18 | 19 | // valid Promise.reject() 20 | 'Promise.reject()', 21 | 'Promise.reject(1)', 22 | 'Promise.reject({})', 23 | 'Promise.reject(referenceToSomething)', 24 | 'Promise.reject(Error())', 25 | 26 | // valid Promise.race() 27 | 'Promise.race([])', 28 | 'Promise.race(iterable)', 29 | 'Promise.race([one, two, three])', 30 | 31 | // valid Promise.all() 32 | 'Promise.all([])', 33 | 'Promise.all(iterable)', 34 | 'Promise.all([one, two, three])', 35 | 36 | // valid Promise.allSettled() 37 | 'Promise.allSettled([])', 38 | 'Promise.allSettled(iterable)', 39 | 'Promise.allSettled([one, two, three])', 40 | 41 | // valid Promise.any() 42 | 'Promise.any([])', 43 | 'Promise.any(iterable)', 44 | 'Promise.any([one, two, three])', 45 | 46 | // valid Promise.then() 47 | 'somePromise().then(success)', 48 | 'somePromise().then(success, failure)', 49 | 'promiseReference.then(() => {})', 50 | 'promiseReference.then(() => {}, () => {})', 51 | 52 | // valid Promise.catch() 53 | 'somePromise().catch(callback)', 54 | 'somePromise().catch(err => {})', 55 | 'promiseReference.catch(callback)', 56 | 'promiseReference.catch(err => {})', 57 | 58 | // valid Promise.finally() 59 | 'somePromise().finally(callback)', 60 | 'somePromise().finally(() => {})', 61 | 'promiseReference.finally(callback)', 62 | 'promiseReference.finally(() => {})', 63 | 64 | { 65 | code: ` 66 | somePromise.then(function() { 67 | return sth(); 68 | }).catch(TypeError, function(e) { 69 | // 70 | }).catch(function(e) { 71 | }); 72 | `, 73 | options: [ 74 | { 75 | exclude: ['catch'], 76 | }, 77 | ], 78 | }, 79 | 80 | // integration test 81 | ` 82 | Promise.all([ 83 | Promise.resolve(1), 84 | Promise.resolve(2), 85 | Promise.reject(Error()), 86 | ]) 87 | .then(console.log) 88 | .catch(console.error) 89 | .finally(console.log) 90 | `, 91 | ], 92 | invalid: [ 93 | // invalid Promise.resolve() 94 | { 95 | code: 'Promise.resolve(1, 2)', 96 | errors: [ 97 | { 98 | message: 99 | 'Promise.resolve() requires 0 or 1 arguments, but received 2', 100 | }, 101 | ], 102 | }, 103 | { 104 | code: 'Promise.resolve({}, function() {}, 1, 2, 3)', 105 | errors: [ 106 | { 107 | message: 108 | 'Promise.resolve() requires 0 or 1 arguments, but received 5', 109 | }, 110 | ], 111 | }, 112 | 113 | // invalid Promise.reject() 114 | { 115 | code: 'Promise.reject(1, 2, 3)', 116 | errors: [ 117 | { 118 | message: 'Promise.reject() requires 0 or 1 arguments, but received 3', 119 | }, 120 | ], 121 | }, 122 | { 123 | code: 'Promise.reject({}, function() {}, 1, 2)', 124 | errors: [ 125 | { 126 | message: 'Promise.reject() requires 0 or 1 arguments, but received 4', 127 | }, 128 | ], 129 | }, 130 | 131 | // invalid Promise.race() 132 | { 133 | code: 'Promise.race(1, 2)', 134 | errors: [ 135 | { message: 'Promise.race() requires 1 argument, but received 2' }, 136 | ], 137 | }, 138 | { 139 | code: 'Promise.race({}, function() {}, 1, 2, 3)', 140 | errors: [ 141 | { message: 'Promise.race() requires 1 argument, but received 5' }, 142 | ], 143 | }, 144 | 145 | // invalid Promise.all() 146 | { 147 | code: 'Promise.all(1, 2, 3)', 148 | errors: [ 149 | { message: 'Promise.all() requires 1 argument, but received 3' }, 150 | ], 151 | }, 152 | { 153 | code: 'Promise.all({}, function() {}, 1, 2)', 154 | errors: [ 155 | { message: 'Promise.all() requires 1 argument, but received 4' }, 156 | ], 157 | }, 158 | // invalid Promise.allSettled() 159 | { 160 | code: 'Promise.allSettled(1, 2, 3)', 161 | errors: [ 162 | { message: 'Promise.allSettled() requires 1 argument, but received 3' }, 163 | ], 164 | }, 165 | { 166 | code: 'Promise.allSettled({}, function() {}, 1, 2)', 167 | errors: [ 168 | { message: 'Promise.allSettled() requires 1 argument, but received 4' }, 169 | ], 170 | }, 171 | // invalid Promise.any() 172 | { 173 | code: 'Promise.any(1, 2, 3)', 174 | errors: [ 175 | { message: 'Promise.any() requires 1 argument, but received 3' }, 176 | ], 177 | }, 178 | { 179 | code: 'Promise.any({}, function() {}, 1, 2)', 180 | errors: [ 181 | { message: 'Promise.any() requires 1 argument, but received 4' }, 182 | ], 183 | }, 184 | 185 | // invalid Promise.then() 186 | { 187 | code: 'somePromise().then()', 188 | errors: [ 189 | { message: 'Promise.then() requires 1 or 2 arguments, but received 0' }, 190 | ], 191 | }, 192 | { 193 | code: 'somePromise().then(() => {}, () => {}, () => {})', 194 | errors: [ 195 | { message: 'Promise.then() requires 1 or 2 arguments, but received 3' }, 196 | ], 197 | }, 198 | { 199 | code: 'promiseReference.then()', 200 | errors: [ 201 | { message: 'Promise.then() requires 1 or 2 arguments, but received 0' }, 202 | ], 203 | }, 204 | { 205 | code: 'promiseReference.then(() => {}, () => {}, () => {})', 206 | errors: [ 207 | { message: 'Promise.then() requires 1 or 2 arguments, but received 3' }, 208 | ], 209 | }, 210 | 211 | // invalid Promise.catch() 212 | { 213 | code: 'somePromise().catch()', 214 | errors: [ 215 | { message: 'Promise.catch() requires 1 argument, but received 0' }, 216 | ], 217 | }, 218 | { 219 | code: 'somePromise().catch(() => {}, () => {})', 220 | errors: [ 221 | { message: 'Promise.catch() requires 1 argument, but received 2' }, 222 | ], 223 | }, 224 | { 225 | code: 'promiseReference.catch()', 226 | errors: [ 227 | { message: 'Promise.catch() requires 1 argument, but received 0' }, 228 | ], 229 | }, 230 | { 231 | code: 'promiseReference.catch(() => {}, () => {})', 232 | errors: [ 233 | { message: 'Promise.catch() requires 1 argument, but received 2' }, 234 | ], 235 | }, 236 | 237 | // invalid Promise.finally() 238 | { 239 | code: 'somePromise().finally()', 240 | errors: [ 241 | { message: 'Promise.finally() requires 1 argument, but received 0' }, 242 | ], 243 | }, 244 | { 245 | code: 'somePromise().finally(() => {}, () => {})', 246 | errors: [ 247 | { message: 'Promise.finally() requires 1 argument, but received 2' }, 248 | ], 249 | }, 250 | { 251 | code: 'promiseReference.finally()', 252 | errors: [ 253 | { message: 'Promise.finally() requires 1 argument, but received 0' }, 254 | ], 255 | }, 256 | { 257 | code: 'promiseReference.finally(() => {}, () => {})', 258 | errors: [ 259 | { message: 'Promise.finally() requires 1 argument, but received 2' }, 260 | ], 261 | }, 262 | ], 263 | }) 264 | -------------------------------------------------------------------------------- /__tests__/no-return-wrap.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/no-return-wrap') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 6, 8 | }, 9 | }) 10 | 11 | const rejectMessage = 'Expected throw instead of Promise.reject' 12 | const resolveMessage = 'Avoid wrapping return values in Promise.resolve' 13 | 14 | ruleTester.run('no-return-wrap', rule, { 15 | valid: [ 16 | // resolve and reject are sometimes okay 17 | 'Promise.resolve(4).then(function(x) { return x })', 18 | 'Promise.reject(4).then(function(x) { return x })', 19 | 'Promise.resolve(4).then(function() {})', 20 | 'Promise.reject(4).then(function() {})', 21 | 22 | // throw and return are fine 23 | 'doThing().then(function() { return 4 })', 24 | 'doThing().then(function() { throw 4 })', 25 | 'doThing().then(null, function() { return 4 })', 26 | 'doThing().then(null, function() { throw 4 })', 27 | 'doThing().catch(null, function() { return 4 })', 28 | 'doThing().catch(null, function() { throw 4 })', 29 | 30 | // other Promise.things are fine 31 | 'doThing().then(function() { return Promise.all([a,b,c]) })', 32 | 33 | // arrow functions and other things 34 | 'doThing().then(() => 4)', 35 | 'doThing().then(() => { throw 4 })', 36 | 'doThing().then(()=>{}, () => 4)', 37 | 'doThing().then(()=>{}, () => { throw 4 })', 38 | 'doThing().catch(() => 4)', 39 | 'doThing().catch(() => { throw 4 })', 40 | 41 | // random functions and callback methods 42 | 'var x = function() { return Promise.resolve(4) }', 43 | 'function y() { return Promise.resolve(4) }', 44 | 'function then() { return Promise.reject() }', 45 | 'doThing(function(x) { return Promise.reject(x) })', 46 | 47 | // should work with empty return statement 48 | 'doThing().then(function() { return })', 49 | 50 | // allow reject if specified 51 | { 52 | code: 'doThing().then(function() { return Promise.reject(4) })', 53 | options: [{ allowReject: true }], 54 | }, 55 | 56 | // not function bind 57 | 'doThing().then((function() { return Promise.resolve(4) }).toString())', 58 | 59 | { 60 | code: 'doThing().then(() => Promise.reject(4))', 61 | options: [{ allowReject: true }], 62 | }, 63 | 64 | // Call expressions that aren't Promise.resolve/reject 65 | 'doThing().then(function() { return a() })', 66 | 'doThing().then(function() { return Promise.a() })', 67 | 'doThing().then(() => { return a() })', 68 | 'doThing().then(() => { return Promise.a() })', 69 | 'doThing().then(() => a())', 70 | 'doThing().then(() => Promise.a())', 71 | ], 72 | 73 | invalid: [ 74 | // wrapped resolve is bad 75 | { 76 | code: 'doThing().then(function() { return Promise.resolve(4) })', 77 | errors: [{ message: resolveMessage }], 78 | }, 79 | { 80 | code: 'doThing().then(null, function() { return Promise.resolve(4) })', 81 | errors: [{ message: resolveMessage }], 82 | }, 83 | { 84 | code: 'doThing().catch(function() { return Promise.resolve(4) })', 85 | errors: [{ message: resolveMessage }], 86 | }, 87 | 88 | // wrapped reject is bad 89 | { 90 | code: 'doThing().then(function() { return Promise.reject(4) })', 91 | errors: [{ message: rejectMessage }], 92 | }, 93 | { 94 | code: 'doThing().then(null, function() { return Promise.reject(4) })', 95 | errors: [{ message: rejectMessage }], 96 | }, 97 | { 98 | code: 'doThing().catch(function() { return Promise.reject(4) })', 99 | errors: [{ message: rejectMessage }], 100 | }, 101 | 102 | // needs to also look at weird paths 103 | { 104 | code: 'doThing().then(function(x) { if (x>1) { return Promise.resolve(4) } else { throw "bad" } })', 105 | errors: [{ message: resolveMessage }], 106 | }, 107 | { 108 | code: 'doThing().then(function(x) { if (x>1) { return Promise.reject(4) } })', 109 | errors: [{ message: rejectMessage }], 110 | }, 111 | { 112 | code: 'doThing().then(null, function() { if (true && false) { return Promise.resolve() } })', 113 | errors: [{ message: resolveMessage }], 114 | }, 115 | 116 | // should do both 117 | { 118 | code: 'doThing().catch(function(x) {if (x) { return Promise.resolve(4) } else { return Promise.reject() } })', 119 | errors: [{ message: resolveMessage }, { message: rejectMessage }], 120 | }, 121 | 122 | // should work someday 123 | // {code: 'doThing().catch(function(x) { return x && Promise.resolve(4) })', errors: [{message: resolveMessage}]}, 124 | // {code: 'doThing().catch(function(x) { return true ? Promise.resolve(4) : Promise.reject(5) })', errors: [{message: rejectMessage }, {message: resolveMessage}]}, 125 | // {code: 'doThing().catch(function(x) { return x && Promise.reject(4) })', errors: [{message: rejectMessage}]} 126 | 127 | // multiple "ExpressionStatement" 128 | { 129 | code: ` 130 | fn(function() { 131 | doThing().then(function() { 132 | return Promise.resolve(4) 133 | }) 134 | return 135 | })`, 136 | errors: [{ message: resolveMessage, line: 4 }], 137 | }, 138 | { 139 | code: ` 140 | fn(function() { 141 | doThing().then(function nm() { 142 | return Promise.resolve(4) 143 | }) 144 | return 145 | })`, 146 | errors: [{ message: resolveMessage, line: 4 }], 147 | }, 148 | { 149 | code: ` 150 | fn(function() { 151 | fn2(function() { 152 | doThing().then(function() { 153 | return Promise.resolve(4) 154 | }) 155 | }) 156 | })`, 157 | errors: [{ message: resolveMessage, line: 5 }], 158 | }, 159 | { 160 | code: ` 161 | fn(function() { 162 | fn2(function() { 163 | doThing().then(function() { 164 | fn3(function() { 165 | return Promise.resolve(4) 166 | }) 167 | return Promise.resolve(4) 168 | }) 169 | }) 170 | })`, 171 | errors: [{ message: resolveMessage, line: 8 }], 172 | }, 173 | 174 | // other than "ExpressionStatement" 175 | { 176 | code: ` 177 | const o = { 178 | fn: function() { 179 | return doThing().then(function() { 180 | return Promise.resolve(5); 181 | }); 182 | }, 183 | } 184 | `, 185 | errors: [{ message: resolveMessage, line: 5 }], 186 | }, 187 | { 188 | code: ` 189 | fn( 190 | doThing().then(function() { 191 | return Promise.resolve(5); 192 | }) 193 | ); 194 | `, 195 | errors: [{ message: resolveMessage, line: 4 }], 196 | }, 197 | 198 | // function bind 199 | { 200 | code: 'doThing().then((function() { return Promise.resolve(4) }).bind(this))', 201 | errors: [{ message: resolveMessage }], 202 | }, 203 | { 204 | code: 'doThing().then((function() { return Promise.resolve(4) }).bind(this).bind(this))', 205 | errors: [{ message: resolveMessage }], 206 | }, 207 | 208 | // arrow functions and other things 209 | { 210 | code: 'doThing().then(() => { return Promise.resolve(4) })', 211 | errors: [{ message: resolveMessage }], 212 | }, 213 | 214 | // issue #150 215 | { 216 | code: ` 217 | function a () { 218 | return p.then(function(val) { 219 | return Promise.resolve(val * 4) 220 | }) 221 | } 222 | `, 223 | errors: [{ message: resolveMessage }], 224 | }, 225 | 226 | // issue #193 227 | { 228 | code: 'doThing().then(() => Promise.resolve(4))', 229 | errors: [{ message: resolveMessage }], 230 | }, 231 | { 232 | code: 'doThing().then(() => Promise.reject(4))', 233 | errors: [{ message: rejectMessage }], 234 | }, 235 | ], 236 | }) 237 | -------------------------------------------------------------------------------- /__tests__/always-return.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/always-return') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 11, 8 | }, 9 | }) 10 | 11 | const message = 'Each then() should return a value or throw' 12 | 13 | ruleTester.run('always-return', rule, { 14 | valid: [ 15 | 'hey.then(x => x)', 16 | 'hey.then(x => ({}))', 17 | 'hey.then(x => { return; })', 18 | 'hey.then(x => { return x ? x.id : null })', 19 | 'hey.then(x => { return x * 10 })', 20 | 'hey.then(x => { process.exit(0); })', 21 | 'hey.then(x => { process.abort(); })', 22 | 'hey.then(function() { return 42; })', 23 | 'hey.then(function() { return new Promise(); })', 24 | 'hey.then(function() { return "x"; }).then(doSomethingWicked)', 25 | 'hey.then(x => x).then(function() { return "3" })', 26 | 'hey.then(function() { throw new Error("msg"); })', 27 | 'hey.then(function(x) { if (!x) { throw new Error("no x"); } return x; })', 28 | 'hey.then(function(x) { if (x) { return x; } throw new Error("no x"); })', 29 | 'hey.then(function(x) { if (x) { process.exit(0); } throw new Error("no x"); })', 30 | 'hey.then(function(x) { if (x) { process.abort(); } throw new Error("no x"); })', 31 | 'hey.then(x => { throw new Error("msg"); })', 32 | 'hey.then(x => { if (!x) { throw new Error("no x"); } return x; })', 33 | 'hey.then(x => { if (x) { return x; } throw new Error("no x"); })', 34 | 'hey.then(x => { var f = function() { }; return f; })', 35 | 'hey.then(x => { if (x) { return x; } else { return x; } })', 36 | 'hey.then(x => { return x; var y = "unreachable"; })', 37 | 'hey.then(x => { return x; return "unreachable"; })', 38 | 'hey.then(x => { return; }, err=>{ log(err); })', 39 | 'hey.then(x => { return x && x(); }, err=>{ log(err); })', 40 | 'hey.then(x => { return x.y || x(); }, err=>{ log(err); })', 41 | `hey.then(x => { 42 | return anotherFunc({ 43 | nested: { 44 | one: x === 1 ? 1 : 0, 45 | two: x === 2 ? 1 : 0 46 | } 47 | }) 48 | })`, 49 | `hey.then(({x, y}) => { 50 | if (y) { 51 | throw new Error(x || y) 52 | } 53 | return x 54 | })`, 55 | { 56 | code: 'hey.then(x => { console.log(x) })', 57 | options: [{ ignoreLastCallback: true }], 58 | }, 59 | { 60 | code: 'if(foo) { hey.then(x => { console.log(x) }) }', 61 | options: [{ ignoreLastCallback: true }], 62 | }, 63 | { 64 | code: 'void hey.then(x => { console.log(x) })', 65 | options: [{ ignoreLastCallback: true }], 66 | }, 67 | { 68 | code: ` 69 | async function foo() { 70 | await hey.then(x => { console.log(x) }) 71 | }`, 72 | options: [{ ignoreLastCallback: true }], 73 | }, 74 | { 75 | code: `hey?.then(x => { console.log(x) })`, 76 | options: [{ ignoreLastCallback: true }], 77 | }, 78 | { 79 | code: `foo = (hey.then(x => { console.log(x) }), 42)`, 80 | options: [{ ignoreLastCallback: true }], 81 | }, 82 | { 83 | code: `(42, hey.then(x => { console.log(x) }))`, 84 | options: [{ ignoreLastCallback: true }], 85 | }, 86 | { 87 | code: ` 88 | hey 89 | .then(x => { console.log(x) }) 90 | .catch(e => console.error(e))`, 91 | options: [{ ignoreLastCallback: true }], 92 | }, 93 | { 94 | code: ` 95 | hey 96 | .then(x => { console.log(x) }) 97 | .catch(e => console.error(e)) 98 | .finally(() => console.error('end'))`, 99 | options: [{ ignoreLastCallback: true }], 100 | }, 101 | { 102 | code: ` 103 | hey 104 | .then(x => { console.log(x) }) 105 | .finally(() => console.error('end'))`, 106 | options: [{ ignoreLastCallback: true }], 107 | }, 108 | `hey.then(x => { globalThis = x })`, 109 | `hey.then(x => { globalThis[a] = x })`, 110 | `hey.then(x => { globalThis.a = x })`, 111 | `hey.then(x => { globalThis.a.n = x })`, 112 | `hey.then(x => { globalThis[12] = x })`, 113 | `hey.then(x => { globalThis['12']["test"] = x })`, 114 | { 115 | code: `hey.then(x => { window['x'] = x })`, 116 | options: [{ ignoreAssignmentVariable: ['globalThis', 'window'] }], 117 | }, 118 | ], 119 | 120 | invalid: [ 121 | { 122 | code: 'hey.then(x => {})', 123 | errors: [{ message }], 124 | }, 125 | { 126 | code: 'hey.then(function() { })', 127 | errors: [{ message }], 128 | }, 129 | { 130 | code: 'hey.then(function() { }).then(x)', 131 | errors: [{ message }], 132 | }, 133 | { 134 | code: 'hey.then(function() { }).then(function() { })', 135 | errors: [{ message }, { message }], 136 | }, 137 | { 138 | code: 'hey.then(function() { return; }).then(function() { })', 139 | errors: [{ message }], 140 | }, 141 | { 142 | code: 'hey.then(function() { doSomethingWicked(); })', 143 | errors: [{ message }], 144 | }, 145 | { 146 | code: 'hey.then(function() { if (x) { return x; } })', 147 | errors: [{ message }], 148 | }, 149 | { 150 | code: 'hey.then(function() { if (x) { return x; } else { }})', 151 | errors: [{ message }], 152 | }, 153 | { 154 | code: 'hey.then(function() { if (x) { } else { return x; }})', 155 | errors: [{ message }], 156 | }, 157 | { 158 | code: 'hey.then(function() { if (x) { process.chdir(); } else { return x; }})', 159 | errors: [{ message }], 160 | }, 161 | { 162 | code: 'hey.then(function() { if (x) { return you.then(function() { return x; }); } })', 163 | errors: [{ message }], 164 | }, 165 | { 166 | code: 'hey.then( x => { x ? x.id : null })', 167 | errors: [{ message }], 168 | }, 169 | { 170 | code: 'hey.then(function(x) { x ? x.id : null })', 171 | errors: [{ message }], 172 | }, 173 | { 174 | code: `(function() { 175 | return hey.then(x => { 176 | anotherFunc({ 177 | nested: { 178 | one: x === 1 ? 1 : 0, 179 | two: x === 2 ? 1 : 0 180 | } 181 | }) 182 | }) 183 | })()`, 184 | errors: [{ message }], 185 | }, 186 | { 187 | code: ` 188 | hey.then(({x, y}) => { 189 | if (y) { 190 | throw new Error(x || y) 191 | } 192 | })`, 193 | errors: [{ message }], 194 | }, 195 | { 196 | code: ` 197 | hey.then(({x, y}) => { 198 | if (y) { 199 | return x 200 | } 201 | })`, 202 | errors: [{ message }], 203 | }, 204 | { 205 | code: ` 206 | hey 207 | .then(function(x) { console.log(x) /* missing return here */ }) 208 | .then(function(y) { console.log(y) /* no error here */ })`, 209 | options: [{ ignoreLastCallback: true }], 210 | errors: [{ message, line: 3 }], 211 | }, 212 | { 213 | code: `const foo = hey.then(function(x) {});`, 214 | options: [{ ignoreLastCallback: true }], 215 | errors: [{ message }], 216 | }, 217 | { 218 | code: ` 219 | function foo() { 220 | return hey.then(function(x) {}); 221 | }`, 222 | options: [{ ignoreLastCallback: true }], 223 | errors: [{ message }], 224 | }, 225 | { 226 | code: ` 227 | async function foo() { 228 | return await hey.then(x => { console.log(x) }) 229 | }`, 230 | options: [{ ignoreLastCallback: true }], 231 | errors: [{ message }], 232 | }, 233 | { 234 | code: `const foo = hey?.then(x => { console.log(x) })`, 235 | options: [{ ignoreLastCallback: true }], 236 | errors: [{ message }], 237 | }, 238 | { 239 | code: `const foo = (42, hey.then(x => { console.log(x) }))`, 240 | options: [{ ignoreLastCallback: true }], 241 | errors: [{ message }], 242 | }, 243 | { 244 | code: `hey.then(x => { invalid = x })`, 245 | errors: [{ message }], 246 | }, 247 | { 248 | code: `hey.then(x => { invalid['x'] = x })`, 249 | errors: [{ message }], 250 | }, 251 | { 252 | code: `hey.then(x => { windo[x] = x })`, 253 | options: [{ ignoreAssignmentVariable: ['window'] }], 254 | errors: [{ message }], 255 | }, 256 | { 257 | code: `hey.then(x => { windo['x'] = x })`, 258 | options: [{ ignoreAssignmentVariable: ['window'] }], 259 | errors: [{ message }], 260 | }, 261 | { 262 | code: `hey.then(x => { windows['x'] = x })`, 263 | options: [{ ignoreAssignmentVariable: ['window'] }], 264 | errors: [{ message }], 265 | }, 266 | { 267 | code: `hey.then(x => { x() })`, 268 | options: [{ ignoreAssignmentVariable: ['window'] }], 269 | errors: [{ message }], 270 | }, 271 | ], 272 | }) 273 | -------------------------------------------------------------------------------- /rules/always-return.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const getDocsUrl = require('./lib/get-docs-url') 4 | 5 | /** 6 | * @typedef {import('estree').Node} Node 7 | * @typedef {import('estree').SimpleCallExpression} CallExpression 8 | * @typedef {import('estree').FunctionExpression} FunctionExpression 9 | * @typedef {import('estree').ArrowFunctionExpression} ArrowFunctionExpression 10 | * @typedef {import('eslint').Rule.CodePath} CodePath 11 | * @typedef {import('eslint').Rule.CodePathSegment} CodePathSegment 12 | */ 13 | 14 | /** 15 | * @typedef { (FunctionExpression | ArrowFunctionExpression) & { parent: CallExpression }} InlineThenFunctionExpression 16 | */ 17 | 18 | /** @param {Node} node */ 19 | function isFunctionWithBlockStatement(node) { 20 | if (node.type === 'FunctionExpression') { 21 | return true 22 | } 23 | if (node.type === 'ArrowFunctionExpression') { 24 | return node.body.type === 'BlockStatement' 25 | } 26 | return false 27 | } 28 | 29 | /** 30 | * @param {string} memberName 31 | * @param {Node} node 32 | * @returns {node is CallExpression} 33 | */ 34 | function isMemberCall(memberName, node) { 35 | return ( 36 | node.type === 'CallExpression' && 37 | node.callee.type === 'MemberExpression' && 38 | !node.callee.computed && 39 | node.callee.property.type === 'Identifier' && 40 | node.callee.property.name === memberName 41 | ) 42 | } 43 | 44 | /** @param {Node} node */ 45 | function isFirstArgument(node) { 46 | return Boolean( 47 | node.parent && node.parent.arguments && node.parent.arguments[0] === node, 48 | ) 49 | } 50 | 51 | /** 52 | * @param {Node} node 53 | * @returns {node is InlineThenFunctionExpression} 54 | */ 55 | function isInlineThenFunctionExpression(node) { 56 | return ( 57 | isFunctionWithBlockStatement(node) && 58 | isMemberCall('then', node.parent) && 59 | isFirstArgument(node) 60 | ) 61 | } 62 | 63 | /** 64 | * Checks whether the given node is the last `then()` callback in a promise chain. 65 | * @param {InlineThenFunctionExpression} node 66 | */ 67 | function isLastCallback(node) { 68 | /** @type {Node} */ 69 | let target = node.parent 70 | /** @type {Node | undefined} */ 71 | let parent = target.parent 72 | while (parent) { 73 | if (parent.type === 'ExpressionStatement') { 74 | // e.g. { promise.then(() => value) } 75 | return true 76 | } 77 | if (parent.type === 'UnaryExpression') { 78 | // e.g. void promise.then(() => value) 79 | return parent.operator === 'void' 80 | } 81 | /** @type {Node | null} */ 82 | let nextTarget = null 83 | if (parent.type === 'SequenceExpression') { 84 | if (peek(parent.expressions) !== target) { 85 | // e.g. (promise?.then(() => value), expr) 86 | return true 87 | } 88 | nextTarget = parent 89 | } else if ( 90 | // e.g. promise?.then(() => value) 91 | parent.type === 'ChainExpression' || 92 | // e.g. await promise.then(() => value) 93 | parent.type === 'AwaitExpression' 94 | ) { 95 | nextTarget = parent 96 | } else if (parent.type === 'MemberExpression') { 97 | if ( 98 | parent.parent && 99 | (isMemberCall('catch', parent.parent) || 100 | isMemberCall('finally', parent.parent)) 101 | ) { 102 | // e.g. promise.then(() => value).catch(e => {}) 103 | nextTarget = parent.parent 104 | } 105 | } 106 | if (nextTarget) { 107 | target = nextTarget 108 | parent = target.parent 109 | continue 110 | } 111 | return false 112 | } 113 | 114 | // istanbul ignore next 115 | return false 116 | } 117 | 118 | /** 119 | * @template T 120 | * @param {T[]} arr 121 | * @returns {T} 122 | */ 123 | function peek(arr) { 124 | return arr[arr.length - 1] 125 | } 126 | 127 | /** 128 | * Gets the root object name for a MemberExpression or Identifier. 129 | * @param {Node} node 130 | * @returns {string | undefined} 131 | */ 132 | function getRootObjectName(node) { 133 | if (node.type === 'Identifier') { 134 | return node.name 135 | } 136 | // istanbul ignore else (fallback) 137 | if (node.type === 'MemberExpression') { 138 | return getRootObjectName(node.object) 139 | } 140 | } 141 | 142 | /** 143 | * Checks if the node is an assignment to an ignored variable. 144 | * @param {Node} node 145 | * @param {string[]} ignoredVars 146 | * @returns {boolean} 147 | */ 148 | function isIgnoredAssignment(node, ignoredVars) { 149 | if (node.type !== 'ExpressionStatement') return false 150 | const expr = node.expression 151 | if (expr.type !== 'AssignmentExpression') return false 152 | const left = expr.left 153 | const rootName = getRootObjectName(left) 154 | return ignoredVars.includes(rootName) 155 | } 156 | 157 | module.exports = { 158 | meta: { 159 | type: 'problem', 160 | docs: { 161 | description: 162 | 'Require returning inside each `then()` to create readable and reusable Promise chains.', 163 | url: getDocsUrl('always-return'), 164 | }, 165 | schema: [ 166 | { 167 | type: 'object', 168 | properties: { 169 | ignoreLastCallback: { 170 | type: 'boolean', 171 | }, 172 | ignoreAssignmentVariable: { 173 | type: 'array', 174 | items: { 175 | type: 'string', 176 | pattern: '^[\\w$]+$', 177 | }, 178 | uniqueItems: true, 179 | }, 180 | }, 181 | additionalProperties: false, 182 | }, 183 | ], 184 | messages: { 185 | thenShouldReturnOrThrow: 'Each then() should return a value or throw', 186 | }, 187 | }, 188 | create(context) { 189 | const options = context.options[0] || {} 190 | const ignoreLastCallback = !!options.ignoreLastCallback 191 | const ignoreAssignmentVariable = options.ignoreAssignmentVariable || [ 192 | 'globalThis', 193 | ] 194 | 195 | /** 196 | * @typedef {object} FuncInfo 197 | * @property {string[]} branchIDStack This is a stack representing the currently 198 | * executing branches ("codePathSegment"s) within the given function 199 | * @property {Record} branchInfoMap This is an object representing information 200 | * about all branches within the given function 201 | * 202 | * @typedef {object} BranchInfo 203 | * @property {boolean} good This is a boolean representing whether 204 | * the given branch explicitly `return`s or `throw`s. It starts as `false` 205 | * for every branch and is updated to `true` if a `return` or `throw` 206 | * statement is found 207 | * @property {Node} node This is a estree Node object 208 | * for the given branch 209 | */ 210 | 211 | /** 212 | * funcInfoStack is a stack representing the stack of currently executing 213 | * functions 214 | * example: 215 | * funcInfoStack = [ { branchIDStack: [ 's1_1' ], 216 | * branchInfoMap: 217 | * { s1_1: 218 | * { good: false, 219 | * loc: } } }, 220 | * { branchIDStack: ['s2_1', 's2_4'], 221 | * branchInfoMap: 222 | * { s2_1: 223 | * { good: false, 224 | * loc: }, 225 | * s2_2: 226 | * { good: true, 227 | * loc: }, 228 | * s2_4: 229 | * { good: false, 230 | * loc: } } } ] 231 | * @type {FuncInfo[]} 232 | */ 233 | const funcInfoStack = [] 234 | 235 | function markCurrentBranchAsGood() { 236 | const funcInfo = peek(funcInfoStack) 237 | const currentBranchID = peek(funcInfo.branchIDStack) 238 | if (funcInfo.branchInfoMap[currentBranchID]) { 239 | funcInfo.branchInfoMap[currentBranchID].good = true 240 | } 241 | // else unreachable code 242 | } 243 | 244 | return { 245 | 'ReturnStatement:exit': markCurrentBranchAsGood, 246 | 'ThrowStatement:exit': markCurrentBranchAsGood, 247 | 'ExpressionStatement > CallExpression > MemberExpression[object.name="process"][property.name="exit"]:exit': 248 | markCurrentBranchAsGood, 249 | 'ExpressionStatement > CallExpression > MemberExpression[object.name="process"][property.name="abort"]:exit': 250 | markCurrentBranchAsGood, 251 | 252 | /** 253 | * @param {CodePathSegment} segment 254 | * @param {Node} node 255 | */ 256 | onCodePathSegmentStart(segment, node) { 257 | const funcInfo = peek(funcInfoStack) 258 | funcInfo.branchIDStack.push(segment.id) 259 | funcInfo.branchInfoMap[segment.id] = { good: false, node } 260 | }, 261 | 262 | onCodePathSegmentEnd() { 263 | const funcInfo = peek(funcInfoStack) 264 | funcInfo.branchIDStack.pop() 265 | }, 266 | 267 | onCodePathStart() { 268 | funcInfoStack.push({ 269 | branchIDStack: [], 270 | branchInfoMap: {}, 271 | }) 272 | }, 273 | 274 | /** 275 | * @param {CodePath} path 276 | * @param {Node} node 277 | */ 278 | onCodePathEnd(path, node) { 279 | const funcInfo = funcInfoStack.pop() 280 | 281 | if (!isInlineThenFunctionExpression(node)) { 282 | return 283 | } 284 | 285 | if (ignoreLastCallback && isLastCallback(node)) { 286 | return 287 | } 288 | 289 | if ( 290 | ignoreAssignmentVariable.length && 291 | isLastCallback(node) && 292 | node.body?.type === 'BlockStatement' 293 | ) { 294 | for (const statement of node.body.body) { 295 | if (isIgnoredAssignment(statement, ignoreAssignmentVariable)) { 296 | return 297 | } 298 | } 299 | } 300 | 301 | path.finalSegments.forEach((segment) => { 302 | const id = segment.id 303 | const branch = funcInfo.branchInfoMap[id] 304 | if (!branch.good) { 305 | context.report({ 306 | messageId: 'thenShouldReturnOrThrow', 307 | node: branch.node, 308 | }) 309 | } 310 | }) 311 | }, 312 | } 313 | }, 314 | } 315 | -------------------------------------------------------------------------------- /__tests__/catch-or-return.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/catch-or-return') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 6, 8 | }, 9 | }) 10 | 11 | const catchMessage = 'Expected catch() or return' 12 | const doneMessage = 'Expected done() or return' 13 | 14 | ruleTester.run('catch-or-return', rule, { 15 | valid: [ 16 | // catch 17 | 'frank().then(go).catch(doIt)', 18 | 'frank().then(go).then().then().then().catch(doIt)', 19 | 'frank().then(go).then().catch(function() { /* why bother */ })', 20 | 'frank.then(go).then(to).catch(jail)', 21 | 'Promise.resolve(frank).catch(jail)', 22 | 'Promise.resolve(frank)["catch"](jail)', 23 | 'frank.then(to).finally(fn).catch(jail)', 24 | 25 | // Cypress 26 | 'cy.get(".myClass").then(go)', 27 | 'cy.get("button").click().then()', 28 | 29 | // arrow function use case 30 | 'postJSON("/smajobber/api/reportJob.json")\n\t.then(()=>this.setState())\n\t.catch(()=>this.setState())', 31 | 32 | // return 33 | 'function a() { return frank().then(go) }', 34 | 'function a() { return frank().then(go).then().then().then() }', 35 | 'function a() { return frank().then(go).then()}', 36 | 'function a() { return frank.then(go).then(to) }', 37 | 38 | // allowThen - .then(null, fn) 39 | { 40 | code: 'frank().then(go).then(null, doIt)', 41 | options: [{ allowThen: true }], 42 | }, 43 | { 44 | code: 'frank().then(go).then().then().then().then(null, doIt)', 45 | options: [{ allowThen: true }], 46 | }, 47 | { 48 | code: 'frank().then(go).then().then(null, function() { /* why bother */ })', 49 | options: [{ allowThen: true }], 50 | }, 51 | { 52 | code: 'frank.then(go).then(to).then(null, jail)', 53 | options: [{ allowThen: true }], 54 | }, 55 | 56 | { 57 | code: 'frank().then(a).then(b).then(null, c)', 58 | options: [{ allowThen: true }], 59 | }, 60 | { 61 | code: 'frank().then(a).then(b).then().then().then(null, doIt)', 62 | options: [{ allowThen: true }], 63 | }, 64 | { 65 | code: 'frank().then(a).then(b).then(null, function() { /* why bother */ })', 66 | options: [{ allowThen: true }], 67 | }, 68 | 69 | // allowThen - .then(fn, fn) 70 | { code: 'frank().then(a, b)', options: [{ allowThen: true }] }, 71 | { 72 | code: 'frank().then(go).then(zam, doIt)', 73 | options: [{ allowThen: true }], 74 | }, 75 | { 76 | code: 'frank().then(a).then(b).then(c, d)', 77 | options: [{ allowThen: true }], 78 | }, 79 | { 80 | code: 'frank().then(go).then().then().then().then(wham, doIt)', 81 | options: [{ allowThen: true }], 82 | }, 83 | { 84 | code: 'frank().then(go).then().then(function() {}, function() { /* why bother */ })', 85 | options: [{ allowThen: true }], 86 | }, 87 | { 88 | code: 'frank.then(go).then(to).then(pewPew, jail)', 89 | options: [{ allowThen: true }], 90 | }, 91 | 92 | // allowThenStrict - .then(null, fn) 93 | { 94 | code: 'frank().then(go).then(null, doIt)', 95 | options: [{ allowThenStrict: true }], 96 | }, 97 | { 98 | code: 'frank().then(go).then().then().then().then(null, doIt)', 99 | options: [{ allowThenStrict: true }], 100 | }, 101 | { 102 | code: 'frank().then(go).then().then(null, function() { /* why bother */ })', 103 | options: [{ allowThenStrict: true }], 104 | }, 105 | { 106 | code: 'frank.then(go).then(to).then(null, jail)', 107 | options: [{ allowThenStrict: true }], 108 | }, 109 | 110 | { 111 | code: 'frank().then(a).then(b).then(null, c)', 112 | options: [{ allowThenStrict: true }], 113 | }, 114 | { 115 | code: 'frank().then(a).then(b).then().then().then(null, doIt)', 116 | options: [{ allowThenStrict: true }], 117 | }, 118 | { 119 | code: 'frank().then(a).then(b).then(null, function() { /* why bother */ })', 120 | options: [{ allowThenStrict: true }], 121 | }, 122 | 123 | // allowFinally - .finally(fn) 124 | { 125 | code: 'frank().then(go).catch(doIt).finally(fn)', 126 | options: [{ allowFinally: true }], 127 | }, 128 | { 129 | code: 'frank().then(go).then().then().then().catch(doIt).finally(fn)', 130 | options: [{ allowFinally: true }], 131 | }, 132 | { 133 | code: 'frank().then(go).then().catch(function() { /* why bother */ }).finally(fn)', 134 | options: [{ allowFinally: true }], 135 | }, 136 | 137 | // terminationMethod=done - .done(null, fn) 138 | { 139 | code: 'frank().then(go).done()', 140 | options: [{ terminationMethod: 'done' }], 141 | }, 142 | 143 | // terminationMethod=[catch, done] - .done(null, fn) 144 | { 145 | code: 'frank().then(go).catch()', 146 | options: [{ terminationMethod: ['catch', 'done'] }], 147 | }, 148 | { 149 | code: 'frank().then(go).done()', 150 | options: [{ terminationMethod: ['catch', 'done'] }], 151 | }, 152 | { 153 | code: 'frank().then(go).finally()', 154 | options: [{ terminationMethod: ['catch', 'finally'] }], 155 | }, 156 | 157 | // for coverage 158 | 'nonPromiseExpressionStatement();', 159 | ], 160 | 161 | invalid: [ 162 | // catch failures 163 | { 164 | code: 'function callPromise(promise, cb) { promise.then(cb) }', 165 | errors: [{ message: catchMessage }], 166 | }, 167 | { 168 | code: 'fetch("http://www.yahoo.com").then(console.log.bind(console))', 169 | errors: [{ message: catchMessage }], 170 | }, 171 | { 172 | code: 'a.then(function() { return "x"; }).then(function(y) { throw y; })', 173 | errors: [{ message: catchMessage }], 174 | }, 175 | { 176 | code: 'Promise.resolve(frank)', 177 | errors: [{ message: catchMessage }], 178 | }, 179 | { 180 | code: 'Promise.all([])', 181 | errors: [{ message: catchMessage }], 182 | }, 183 | { 184 | code: 'Promise.allSettled([])', 185 | errors: [{ message: catchMessage }], 186 | }, 187 | { 188 | code: 'Promise.any([])', 189 | errors: [{ message: catchMessage }], 190 | }, 191 | { 192 | code: 'Promise.race([])', 193 | errors: [{ message: catchMessage }], 194 | }, 195 | { 196 | code: 'frank().then(to).catch(fn).then(foo)', 197 | errors: [{ message: catchMessage }], 198 | }, 199 | { 200 | code: 'frank().finally(fn)', 201 | errors: [{ message: catchMessage }], 202 | }, 203 | { 204 | code: 'frank().then(to).finally(fn)', 205 | errors: [{ message: catchMessage }], 206 | }, 207 | { 208 | code: 'frank().then(go).catch(doIt).finally(fn)', 209 | errors: [{ message: catchMessage }], 210 | }, 211 | { 212 | code: 'frank().then(go).then().then().then().catch(doIt).finally(fn)', 213 | errors: [{ message: catchMessage }], 214 | }, 215 | { 216 | code: 'frank().then(go).then().catch(function() { /* why bother */ }).finally(fn)', 217 | errors: [{ message: catchMessage }], 218 | }, 219 | 220 | // return failures 221 | { 222 | code: 'function a() { frank().then(go) }', 223 | errors: [{ message: catchMessage }], 224 | }, 225 | { 226 | code: 'function a() { frank().then(go).then().then().then() }', 227 | errors: [{ message: catchMessage }], 228 | }, 229 | { 230 | code: 'function a() { frank().then(go).then()}', 231 | errors: [{ message: catchMessage }], 232 | }, 233 | { 234 | code: 'function a() { frank.then(go).then(to) }', 235 | errors: [{ message: catchMessage }], 236 | }, 237 | 238 | // allowFinally=true failures 239 | { 240 | code: 'frank().then(go).catch(doIt).finally(fn).then(foo)', 241 | options: [{ allowFinally: true }], 242 | errors: [{ message: catchMessage }], 243 | }, 244 | { 245 | code: 'frank().then(go).catch(doIt).finally(fn).foobar(foo)', 246 | options: [{ allowFinally: true }], 247 | errors: [{ message: catchMessage }], 248 | }, 249 | 250 | // terminationMethod=done - .done(null, fn) 251 | { 252 | code: 'frank().then(go)', 253 | options: [{ terminationMethod: 'done' }], 254 | errors: [{ message: doneMessage }], 255 | }, 256 | { 257 | code: 'frank().catch(go)', 258 | options: [{ terminationMethod: 'done' }], 259 | errors: [{ message: doneMessage }], 260 | }, 261 | 262 | // assume somePromise.ANYTHING() is a new promise 263 | { 264 | code: 'frank().catch(go).someOtherMethod()', 265 | errors: [{ message: catchMessage }], 266 | }, 267 | 268 | // .then(null, fn) 269 | { 270 | code: 'frank().then(a).then(b).then(null, c)', 271 | errors: [{ message: catchMessage }], 272 | }, 273 | { 274 | code: 'frank().then(a).then(b).then().then().then(null, doIt)', 275 | errors: [{ message: catchMessage }], 276 | }, 277 | { 278 | code: 'frank().then(a).then(b).then(null, function() { /* why bother */ })', 279 | errors: [{ message: catchMessage }], 280 | }, 281 | 282 | // .then(fn, fn) 283 | { 284 | code: 'frank().then(a, b)', 285 | errors: [{ message: catchMessage }], 286 | }, 287 | { 288 | code: 'frank().then(go).then(zam, doIt)', 289 | errors: [{ message: catchMessage }], 290 | }, 291 | { 292 | code: 'frank().then(a).then(b).then(c, d)', 293 | errors: [{ message: catchMessage }], 294 | }, 295 | { 296 | code: 'frank().then(go).then().then().then().then(wham, doIt)', 297 | errors: [{ message: catchMessage }], 298 | }, 299 | { 300 | code: 'frank().then(go).then().then(function() {}, function() { /* why bother */ })', 301 | errors: [{ message: catchMessage }], 302 | }, 303 | { 304 | code: 'frank.then(go).then(to).then(pewPew, jail)', 305 | errors: [{ message: catchMessage }], 306 | }, 307 | 308 | { 309 | code: 'frank().then(a, b)', 310 | errors: [{ message: catchMessage }], 311 | options: [{ allowThenStrict: true }], 312 | }, 313 | { 314 | code: 'frank().then(go).then(zam, doIt)', 315 | errors: [{ message: catchMessage }], 316 | options: [{ allowThenStrict: true }], 317 | }, 318 | { 319 | code: 'frank().then(a).then(b).then(c, d)', 320 | errors: [{ message: catchMessage }], 321 | options: [{ allowThenStrict: true }], 322 | }, 323 | { 324 | code: 'frank().then(go).then().then().then().then(wham, doIt)', 325 | errors: [{ message: catchMessage }], 326 | options: [{ allowThenStrict: true }], 327 | }, 328 | { 329 | code: 'frank().then(go).then().then(function() {}, function() { /* why bother */ })', 330 | errors: [{ message: catchMessage }], 331 | options: [{ allowThenStrict: true }], 332 | }, 333 | { 334 | code: 'frank.then(go).then(to).then(pewPew, jail)', 335 | errors: [{ message: catchMessage }], 336 | options: [{ allowThenStrict: true }], 337 | }, 338 | ], 339 | }) 340 | -------------------------------------------------------------------------------- /__tests__/no-multiple-resolved.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../rules/no-multiple-resolved') 4 | const { RuleTester } = require('./rule-tester') 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 2020, 8 | }, 9 | }) 10 | 11 | ruleTester.run('no-multiple-resolved', rule, { 12 | valid: [ 13 | `new Promise((resolve, reject) => { 14 | fn((error, value) => { 15 | if (error) { 16 | reject(error) 17 | } else { 18 | resolve(value) 19 | } 20 | }) 21 | })`, 22 | `new Promise((resolve, reject) => { 23 | if (error) { 24 | reject(error) 25 | } else { 26 | resolve(value) 27 | } 28 | })`, 29 | `new Promise((resolve, reject) => { 30 | fn((error, value) => { 31 | if (error) { 32 | reject(error) 33 | return 34 | } 35 | resolve(value) 36 | }) 37 | })`, 38 | `new Promise((resolve, reject) => { 39 | fn((error, value) => { 40 | if (error) { 41 | reject(error) 42 | } 43 | if (!error) { 44 | resolve(value) 45 | } 46 | }) 47 | })`, 48 | `new Promise((resolve, reject) => { 49 | fn((error, value) => { 50 | if (error) { 51 | reject(error) 52 | } 53 | if (error) { 54 | return 55 | } 56 | resolve(value) 57 | }) 58 | })`, 59 | ` 60 | new Promise((resolve, reject) => { 61 | fn((error, value) => { 62 | if (error) { 63 | reject(error) 64 | } 65 | if (!error) { 66 | // process 67 | } else { 68 | // process 69 | } 70 | if(!error) { 71 | resolve(value) 72 | } 73 | }) 74 | })`, 75 | ` 76 | new Promise((resolve, reject) => { 77 | fn((error, value) => { 78 | if (error) { 79 | reject(error) 80 | return 81 | } 82 | if (!error) { 83 | // process 84 | } else { 85 | // process 86 | } 87 | 88 | resolve(value) 89 | }) 90 | })`, 91 | `new Promise(async (resolve, reject) => { 92 | try { 93 | await foo(); 94 | resolve(); 95 | } catch (error) { 96 | reject(error); 97 | } 98 | })`, 99 | `new Promise(async (resolve, reject) => { 100 | try { 101 | const r = await foo(); 102 | resolve(r); 103 | } catch (error) { 104 | reject(error); 105 | } 106 | })`, 107 | `new Promise(async (resolve, reject) => { 108 | try { 109 | const r = await foo(); 110 | resolve(r()); 111 | } catch (error) { 112 | reject(error); 113 | } 114 | })`, 115 | `new Promise(async (resolve, reject) => { 116 | try { 117 | const r = await foo(); 118 | resolve(r.foo); 119 | } catch (error) { 120 | reject(error); 121 | } 122 | })`, 123 | `new Promise(async (resolve, reject) => { 124 | try { 125 | const r = await foo(); 126 | resolve(new r()); 127 | } catch (error) { 128 | reject(error); 129 | } 130 | })`, 131 | `new Promise(async (resolve, reject) => { 132 | try { 133 | const r = await foo(); 134 | resolve(import(r)); 135 | } catch (error) { 136 | reject(error); 137 | } 138 | })`, 139 | `new Promise((resolve, reject) => { 140 | fn(async function * () { 141 | try { 142 | const r = await foo(); 143 | resolve(yield r); 144 | } catch (error) { 145 | reject(error); 146 | } 147 | }) 148 | })`, 149 | `new Promise(async (resolve, reject) => { 150 | let a; 151 | try { 152 | const r = await foo(); 153 | resolve(); 154 | if(r) return; 155 | } catch (error) { 156 | reject(error); 157 | } 158 | })`, 159 | ], 160 | 161 | invalid: [ 162 | { 163 | code: ` 164 | new Promise((resolve, reject) => { 165 | fn((error, value) => { 166 | if (error) { 167 | reject(error) 168 | } 169 | 170 | resolve(value) 171 | }) 172 | })`, 173 | errors: [ 174 | { 175 | message: 176 | 'Promise should not be resolved multiple times. Promise is potentially resolved on line 5.', 177 | line: 8, 178 | }, 179 | ], 180 | }, 181 | { 182 | code: ` 183 | new Promise((resolve, reject) => { 184 | if (error) { 185 | reject(error) 186 | } 187 | 188 | resolve(value) 189 | })`, 190 | errors: [ 191 | { 192 | message: 193 | 'Promise should not be resolved multiple times. Promise is potentially resolved on line 4.', 194 | line: 7, 195 | }, 196 | ], 197 | }, 198 | { 199 | code: ` 200 | new Promise((resolve, reject) => { 201 | reject(error) 202 | resolve(value) 203 | })`, 204 | errors: [ 205 | { 206 | message: 207 | 'Promise should not be resolved multiple times. Promise is already resolved on line 3.', 208 | line: 4, 209 | }, 210 | ], 211 | }, 212 | { 213 | code: ` 214 | new Promise((resolve, reject) => { 215 | fn((error, value) => { 216 | if (error) { 217 | reject(error) 218 | } 219 | if (!error) { 220 | // process 221 | } else { 222 | // process 223 | } 224 | 225 | resolve(value) 226 | }) 227 | })`, 228 | errors: [ 229 | { 230 | message: 231 | 'Promise should not be resolved multiple times. Promise is potentially resolved on line 5.', 232 | line: 13, 233 | }, 234 | ], 235 | }, 236 | { 237 | code: ` 238 | new Promise((resolve, reject) => { 239 | fn((error, value) => { 240 | if (error) { 241 | if (foo) { 242 | if (bar) reject(error) 243 | } 244 | } 245 | 246 | resolve(value) 247 | }) 248 | })`, 249 | errors: [ 250 | { 251 | message: 252 | 'Promise should not be resolved multiple times. Promise is potentially resolved on line 6.', 253 | line: 10, 254 | }, 255 | ], 256 | }, 257 | { 258 | code: ` 259 | new Promise((resolve, reject) => { 260 | fn((error, value) => { 261 | if (error) { 262 | reject(error) 263 | } else { 264 | return 265 | } 266 | 267 | resolve(value) 268 | }) 269 | })`, 270 | errors: [ 271 | { 272 | message: 273 | 'Promise should not be resolved multiple times. Promise is already resolved on line 5.', 274 | line: 10, 275 | }, 276 | ], 277 | }, 278 | { 279 | code: ` 280 | new Promise((resolve, reject) => { 281 | if(foo) { 282 | if (error) { 283 | reject(error) 284 | } else { 285 | return 286 | } 287 | resolve(value) 288 | } 289 | 290 | resolve(value) 291 | })`, 292 | errors: [ 293 | { 294 | message: 295 | 'Promise should not be resolved multiple times. Promise is already resolved on line 5.', 296 | line: 9, 297 | }, 298 | { 299 | message: 300 | 'Promise should not be resolved multiple times. Promise is potentially resolved on line 9.', 301 | line: 12, 302 | }, 303 | ], 304 | }, 305 | { 306 | code: ` 307 | new Promise((resolve, reject) => { 308 | if (foo) { 309 | reject(error) 310 | } else { 311 | resolve(value) 312 | } 313 | if(bar) { 314 | resolve(value) 315 | } 316 | })`, 317 | errors: [ 318 | { 319 | message: 320 | 'Promise should not be resolved multiple times. Promise is already resolved on line 4.', 321 | line: 9, 322 | }, 323 | ], 324 | }, 325 | { 326 | code: ` 327 | new Promise((resolve, reject) => { 328 | while (error) { 329 | reject(error) 330 | } 331 | resolve(value) 332 | })`, 333 | errors: [ 334 | { 335 | message: 336 | 'Promise should not be resolved multiple times. Promise is potentially resolved on line 4.', 337 | line: 6, 338 | }, 339 | ], 340 | }, 341 | { 342 | code: ` 343 | new Promise((resolve, reject) => { 344 | try { 345 | reject(error) 346 | } finally { 347 | resolve(value) 348 | } 349 | })`, 350 | errors: [ 351 | { 352 | message: 353 | 'Promise should not be resolved multiple times. Promise is already resolved on line 4.', 354 | line: 6, 355 | }, 356 | ], 357 | }, 358 | { 359 | code: ` 360 | new Promise((resolve, reject) => { 361 | try { 362 | if (error) { 363 | reject(error) 364 | } 365 | } finally { 366 | resolve(value) 367 | } 368 | })`, 369 | errors: [ 370 | { 371 | message: 372 | 'Promise should not be resolved multiple times. Promise is potentially resolved on line 5.', 373 | line: 8, 374 | }, 375 | ], 376 | }, 377 | { 378 | code: `new Promise(async (resolve, reject) => { 379 | try { 380 | const r = await foo(); 381 | resolve(); 382 | r(); 383 | } catch (error) { 384 | reject(error); 385 | } 386 | })`, 387 | errors: [ 388 | { 389 | message: 390 | 'Promise should not be resolved multiple times. Promise is potentially resolved on line 4.', 391 | line: 7, 392 | }, 393 | ], 394 | }, 395 | { 396 | code: `new Promise(async (resolve, reject) => { 397 | let a; 398 | try { 399 | const r = await foo(); 400 | resolve(); 401 | a = r.foo; 402 | } catch (error) { 403 | reject(error); 404 | } 405 | })`, 406 | errors: [ 407 | { 408 | message: 409 | 'Promise should not be resolved multiple times. Promise is potentially resolved on line 5.', 410 | line: 8, 411 | }, 412 | ], 413 | }, 414 | { 415 | code: `new Promise(async (resolve, reject) => { 416 | let a; 417 | try { 418 | const r = await foo(); 419 | resolve(); 420 | a = new r(); 421 | } catch (error) { 422 | reject(error); 423 | } 424 | })`, 425 | errors: [ 426 | { 427 | message: 428 | 'Promise should not be resolved multiple times. Promise is potentially resolved on line 5.', 429 | line: 8, 430 | }, 431 | ], 432 | }, 433 | { 434 | code: `new Promise(async (resolve, reject) => { 435 | let a; 436 | try { 437 | const r = await foo(); 438 | resolve(); 439 | import(r); 440 | } catch (error) { 441 | reject(error); 442 | } 443 | })`, 444 | errors: [ 445 | { 446 | message: 447 | 'Promise should not be resolved multiple times. Promise is potentially resolved on line 5.', 448 | line: 8, 449 | }, 450 | ], 451 | }, 452 | { 453 | code: `new Promise((resolve, reject) => { 454 | fn(async function * () { 455 | try { 456 | const r = await foo(); 457 | resolve(); 458 | yield r; 459 | } catch (error) { 460 | reject(error); 461 | } 462 | }) 463 | })`, 464 | errors: [ 465 | { 466 | message: 467 | 'Promise should not be resolved multiple times. Promise is potentially resolved on line 5.', 468 | line: 8, 469 | }, 470 | ], 471 | }, 472 | ], 473 | }) 474 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [7.2.1](https://github.com/eslint-community/eslint-plugin-promise/compare/v7.2.0...v7.2.1) (2024-11-26) 4 | 5 | 6 | ### 🩹 Fixes 7 | 8 | * **`no-callback-in-promise`:** false triggering of callback ([#574](https://github.com/eslint-community/eslint-plugin-promise/issues/574)) ([8324564](https://github.com/eslint-community/eslint-plugin-promise/commit/83245645a1731b8720ba4b17951f0e98567f449c)) 9 | 10 | 11 | ### 🧹 Chores 12 | 13 | * **deps-dev:** update husky to v9.1.7 ([#573](https://github.com/eslint-community/eslint-plugin-promise/issues/573)) ([24fd90a](https://github.com/eslint-community/eslint-plugin-promise/commit/24fd90a0262e1521983095f0934e9bb0195b4d23)) 14 | * **deps:** bump cross-spawn from 7.0.3 to 7.0.6 ([#568](https://github.com/eslint-community/eslint-plugin-promise/issues/568)) ([f33f82e](https://github.com/eslint-community/eslint-plugin-promise/commit/f33f82e03ee949d2864e266aedfe5da9762ad540)) 15 | 16 | ## [7.2.0](https://github.com/eslint-community/eslint-plugin-promise/compare/v7.1.0...v7.2.0) (2024-11-25) 17 | 18 | 19 | ### 🌟 Features 20 | 21 | * **`no-callback-in-promise`:** add `timeoutsErr` option ([#514](https://github.com/eslint-community/eslint-plugin-promise/issues/514)) ([907753f](https://github.com/eslint-community/eslint-plugin-promise/commit/907753f4b6108ba78b93571a40b6f1384b3c6899)) 22 | * **`valid-params`:** add `exclude` option ([#515](https://github.com/eslint-community/eslint-plugin-promise/issues/515)) ([7ff2cb9](https://github.com/eslint-community/eslint-plugin-promise/commit/7ff2cb9298f5dd0b4dae82321605d04e50ca935b)) 23 | * **always-return:** add `ignoreAssignmentVariable` option ([#518](https://github.com/eslint-community/eslint-plugin-promise/issues/518)) ([701279c](https://github.com/eslint-community/eslint-plugin-promise/commit/701279c573437598e86873f48b4f5cf6432ae38e)) 24 | * **catch-or-return:** add `allowThenStrict` option ([#522](https://github.com/eslint-community/eslint-plugin-promise/issues/522)) ([53be970](https://github.com/eslint-community/eslint-plugin-promise/commit/53be970e91023a104ce3ef2918b3ee80ef265f27)) 25 | * new rule `prefer-catch` ([#525](https://github.com/eslint-community/eslint-plugin-promise/issues/525)) ([05c8a93](https://github.com/eslint-community/eslint-plugin-promise/commit/05c8a930893e6abff2a0a7e1fb82a1543c19df9f)) 26 | 27 | 28 | ### 🩹 Fixes 29 | 30 | * permit appropriate computed member expressions and prototype access ([#535](https://github.com/eslint-community/eslint-plugin-promise/issues/535)) ([4de9d43](https://github.com/eslint-community/eslint-plugin-promise/commit/4de9d43b84f1beb166a7ba779a4da9d732d0eab3)) 31 | 32 | 33 | ### 🧹 Chores 34 | 35 | * **deps-dev:** bump eslint-plugin-jest from 28.6.0 to 28.8.0 ([#536](https://github.com/eslint-community/eslint-plugin-promise/issues/536)) ([80741f8](https://github.com/eslint-community/eslint-plugin-promise/commit/80741f849db526cad362cfc976c69a1df036a6c6)) 36 | * **deps-dev:** bump eslint-plugin-n from 17.9.0 to 17.10.2 ([#529](https://github.com/eslint-community/eslint-plugin-promise/issues/529)) ([a646010](https://github.com/eslint-community/eslint-plugin-promise/commit/a646010a7700a87c0fcc8aa0bb0d580bd6a14fd4)) 37 | * **deps-dev:** bump globals from 15.8.0 to 15.9.0 ([#527](https://github.com/eslint-community/eslint-plugin-promise/issues/527)) ([b8afe92](https://github.com/eslint-community/eslint-plugin-promise/commit/b8afe920bd3be1120f5effb4a9a71451a3e71c24)) 38 | * **deps-dev:** bump husky from 9.1.2 to 9.1.4 ([#524](https://github.com/eslint-community/eslint-plugin-promise/issues/524)) ([b8fdb9f](https://github.com/eslint-community/eslint-plugin-promise/commit/b8fdb9f1d23446d74a9d0976507988dac06684b2)) 39 | * **deps-dev:** bump lint-staged from 15.2.7 to 15.2.8 ([#539](https://github.com/eslint-community/eslint-plugin-promise/issues/539)) ([9e2528f](https://github.com/eslint-community/eslint-plugin-promise/commit/9e2528ffabe91217d0cd12d634dceb70462b9353)) 40 | * **deps-dev:** update eslint-plugin-eslint-plugin to v6.3.0 ([#560](https://github.com/eslint-community/eslint-plugin-promise/issues/560)) ([7459bd6](https://github.com/eslint-community/eslint-plugin-promise/commit/7459bd67b0056d363e3d53de084642eb79b74944)) 41 | * **deps-dev:** update eslint-plugin-eslint-plugin to v6.3.1 ([#561](https://github.com/eslint-community/eslint-plugin-promise/issues/561)) ([434c6fa](https://github.com/eslint-community/eslint-plugin-promise/commit/434c6fa2ed1d8747b28b002ce539fa5ccc2d0921)) 42 | * **deps-dev:** update eslint-plugin-eslint-plugin to v6.3.2 ([#570](https://github.com/eslint-community/eslint-plugin-promise/issues/570)) ([a849f64](https://github.com/eslint-community/eslint-plugin-promise/commit/a849f6467ef90ec2f3c988b9e6591b347287a80a)) 43 | * **deps-dev:** update eslint-plugin-jest to v28.9.0 ([#565](https://github.com/eslint-community/eslint-plugin-promise/issues/565)) ([cf213fb](https://github.com/eslint-community/eslint-plugin-promise/commit/cf213fbab43533f338333b1cb986d4b1041dc51c)) 44 | * **deps-dev:** update eslint-plugin-n to v17.12.0 ([#563](https://github.com/eslint-community/eslint-plugin-promise/issues/563)) ([d39e2f0](https://github.com/eslint-community/eslint-plugin-promise/commit/d39e2f0d6f5cbaa495957aa69be74f4c94113148)) 45 | * **deps-dev:** update eslint-plugin-n to v17.13.0 ([#566](https://github.com/eslint-community/eslint-plugin-promise/issues/566)) ([b62f234](https://github.com/eslint-community/eslint-plugin-promise/commit/b62f2345de7a1d307ff63e761471431cfc2bfb8f)) 46 | * **deps-dev:** update eslint-plugin-n to v17.13.1 ([#567](https://github.com/eslint-community/eslint-plugin-promise/issues/567)) ([266ddbb](https://github.com/eslint-community/eslint-plugin-promise/commit/266ddbb03076c05c362a6daecb9382b80cdd7108)) 47 | * **deps-dev:** update eslint-plugin-n to v17.13.2 ([#569](https://github.com/eslint-community/eslint-plugin-promise/issues/569)) ([390f51f](https://github.com/eslint-community/eslint-plugin-promise/commit/390f51fe07b2d375ec93f52c19a6964637c3ae8c)) 48 | * **deps-dev:** update npm-run-all2 to v6.2.4 ([#558](https://github.com/eslint-community/eslint-plugin-promise/issues/558)) ([2cf1732](https://github.com/eslint-community/eslint-plugin-promise/commit/2cf17322af17311fac773b524fa55589ebe4c9fd)) 49 | * **deps-dev:** update npm-run-all2 to v6.2.6 ([#559](https://github.com/eslint-community/eslint-plugin-promise/issues/559)) ([dc32933](https://github.com/eslint-community/eslint-plugin-promise/commit/dc32933c0d61e2a916a96ee21d37d3058976c090)) 50 | * **deps:** switch from dependabot to renovate using shared eslint community configuration ([#537](https://github.com/eslint-community/eslint-plugin-promise/issues/537)) ([30efed7](https://github.com/eslint-community/eslint-plugin-promise/commit/30efed7cf9e8b49d6368df9ae8be84b9619cf621)) 51 | * **deps:** update @eslint-community/eslint-utils to v4.4.1 ([#562](https://github.com/eslint-community/eslint-plugin-promise/issues/562)) ([5c3628d](https://github.com/eslint-community/eslint-plugin-promise/commit/5c3628de60c4a5f6cbcd9240264397c5f7821f16)) 52 | * **deps:** update globals to v15.12.0 ([#564](https://github.com/eslint-community/eslint-plugin-promise/issues/564)) ([c8632d1](https://github.com/eslint-community/eslint-plugin-promise/commit/c8632d1558f87c5c4761a9e7b5a7f277c8bdfda6)) 53 | * update @typescript-eslint/parser to v7.18.0 ([#545](https://github.com/eslint-community/eslint-plugin-promise/issues/545)) ([5744e60](https://github.com/eslint-community/eslint-plugin-promise/commit/5744e6061059acbd2fe736bd74cd50c5d3fd2808)) 54 | * update dependency eslint-plugin-n to v17.11.0 ([#556](https://github.com/eslint-community/eslint-plugin-promise/issues/556)) ([bbd048b](https://github.com/eslint-community/eslint-plugin-promise/commit/bbd048bdd13e3004f56863fae8221e4e8fcaac77)) 55 | * update dependency eslint-plugin-n to v17.11.1 ([#557](https://github.com/eslint-community/eslint-plugin-promise/issues/557)) ([e545254](https://github.com/eslint-community/eslint-plugin-promise/commit/e5452545904462a5c5574ed506d4d9d6afca6701)) 56 | * update dependency globals to v15.11.0 ([#555](https://github.com/eslint-community/eslint-plugin-promise/issues/555)) ([9151db8](https://github.com/eslint-community/eslint-plugin-promise/commit/9151db8c21c9566ad7c87aad55a75fedba6cb980)) 57 | * update dependency typescript to v5.6.3 ([#554](https://github.com/eslint-community/eslint-plugin-promise/issues/554)) ([ab55120](https://github.com/eslint-community/eslint-plugin-promise/commit/ab55120d425047594db18c4cfb3f5c1f6bd44b61)) 58 | * update eslint to v8.57.1 ([#551](https://github.com/eslint-community/eslint-plugin-promise/issues/551)) ([38e2757](https://github.com/eslint-community/eslint-plugin-promise/commit/38e27571e8583eb014b167fccc37f9b5a90af52f)) 59 | * update eslint-plugin-jest to v28.8.3 ([#548](https://github.com/eslint-community/eslint-plugin-promise/issues/548)) ([89f2578](https://github.com/eslint-community/eslint-plugin-promise/commit/89f257856b919fac252c2a6e742f2c385c7cf25e)) 60 | * update eslint-plugin-n to v17.10.3 ([#552](https://github.com/eslint-community/eslint-plugin-promise/issues/552)) ([2d738fe](https://github.com/eslint-community/eslint-plugin-promise/commit/2d738fedfc162215140c374a6de4a2d2d13c0472)) 61 | * update globals to v15.10.0 ([#553](https://github.com/eslint-community/eslint-plugin-promise/issues/553)) ([b871314](https://github.com/eslint-community/eslint-plugin-promise/commit/b8713140b2e42180a936b21d503273f2aacaea4a)) 62 | * update husky to v9.1.6 ([#547](https://github.com/eslint-community/eslint-plugin-promise/issues/547)) ([1e8d18f](https://github.com/eslint-community/eslint-plugin-promise/commit/1e8d18f56a889d4f1ba327c3554bec84c8e9fcb2)) 63 | * update lint-staged to v15.2.10 ([#544](https://github.com/eslint-community/eslint-plugin-promise/issues/544)) ([7d46b3b](https://github.com/eslint-community/eslint-plugin-promise/commit/7d46b3b0eced0ff31a4e8492b70cd4f363f02d2e)) 64 | * update npm-run-all2 to v6.2.3 ([#550](https://github.com/eslint-community/eslint-plugin-promise/issues/550)) ([14cd4c0](https://github.com/eslint-community/eslint-plugin-promise/commit/14cd4c098e50a6c5d14becafc9f337237015a5cc)) 65 | * update typescript to ~5.6.0 ([#549](https://github.com/eslint-community/eslint-plugin-promise/issues/549)) ([ebcdd8b](https://github.com/eslint-community/eslint-plugin-promise/commit/ebcdd8bc6e2fed8164abf78650a7d45689aa04dc)) 66 | 67 | ## [7.1.0](https://github.com/eslint-community/eslint-plugin-promise/compare/v7.0.0...v7.1.0) (2024-08-06) 68 | 69 | 70 | ### 🌟 Features 71 | 72 | * **`catch-or-return`, `prefer-await-to-then`:** do not report Cypress commands ([#495](https://github.com/eslint-community/eslint-plugin-promise/issues/495)) ([943f162](https://github.com/eslint-community/eslint-plugin-promise/commit/943f16290f11af9717612e079646802e22310290)) 73 | * **`prefer-await-to-then`:** ignore constructor scope unless with `strict` option ([#496](https://github.com/eslint-community/eslint-plugin-promise/issues/496)) ([7bffb7a](https://github.com/eslint-community/eslint-plugin-promise/commit/7bffb7a666ed74a876ba3a6c482c36ea6f9d6d07)) 74 | * new rule `spec-only` to check for non-spec Promise methods ([#502](https://github.com/eslint-community/eslint-plugin-promise/issues/502)) ([d6e9de1](https://github.com/eslint-community/eslint-plugin-promise/commit/d6e9de1f9c81194b775484ed0299dc5cc4898684)) 75 | 76 | 77 | ### 📚 Documentation 78 | 79 | * fixes the CI readme badge ([#511](https://github.com/eslint-community/eslint-plugin-promise/issues/511)) ([030a3be](https://github.com/eslint-community/eslint-plugin-promise/commit/030a3be890d371381ef13258806f97ec62d6b4fd)) 80 | * supply missing docs ([#503](https://github.com/eslint-community/eslint-plugin-promise/issues/503)) ([602d825](https://github.com/eslint-community/eslint-plugin-promise/commit/602d8254871e46c9d1808ee1a3a2c48cb7493334)) 81 | 82 | 83 | ### 🧹 Chores 84 | 85 | * bump dev dependencies ([#483](https://github.com/eslint-community/eslint-plugin-promise/issues/483)) ([197ae4e](https://github.com/eslint-community/eslint-plugin-promise/commit/197ae4eb4f05f34c54189102871d969379595a54)) 86 | * **deps-dev:** bump husky from 9.1.1 to 9.1.2 ([#516](https://github.com/eslint-community/eslint-plugin-promise/issues/516)) ([ab8e7a0](https://github.com/eslint-community/eslint-plugin-promise/commit/ab8e7a0d4fc8bde63fb6a6bb1e9743152778c4ee)) 87 | * file extension missing ([#519](https://github.com/eslint-community/eslint-plugin-promise/issues/519)) ([94c9834](https://github.com/eslint-community/eslint-plugin-promise/commit/94c983483596bca2baa6c710273d348f8cf98d58)) 88 | * fix format.yml ([#507](https://github.com/eslint-community/eslint-plugin-promise/issues/507)) ([948c097](https://github.com/eslint-community/eslint-plugin-promise/commit/948c09776e23e7dc38f155b268dcc002d59a957b)) 89 | 90 | ## 6.0.2 91 | 92 | - Added tests for @typescript-eslint/parser support 93 | 94 | ## 6.0.1 95 | 96 | - Fixed @typescript-eslint/parser issue #331, #205 97 | 98 | ## 6.0.0 99 | 100 | - Dropped node 10 from engines #231 101 | - Updated a ton of deps #236, #237, #235, #234 102 | - ESLint 8 support #219 103 | 104 | ## 5.2.0 105 | 106 | - Updated `param-names` rule to allow for unused params 107 | 108 | ## 5.1.1 109 | 110 | - Updated docs to include `no-callback-in-promise` reasons #215 111 | 112 | ## 5.1.0 113 | 114 | - Included `catch()` and `finally()` in `prefer-await-to-then` #196 115 | - Added some additional tests and upgraded some dev deps #196 116 | - Exempted array methods in prefer-await-to-callbacks 117 | ([#212](https://github.com/eslint-community/eslint-plugin-promise/issues/212)) 118 | 119 | ## 5.0.0 120 | 121 | - ESLint 7.0 Support 122 | 123 | ## 4.3.1. 124 | 125 | - Updated and applied prettier 126 | 127 | ## 4.3.0 128 | 129 | - https://github.com/eslint-community/eslint-plugin-promise/pull/202 130 | - Updated jest 131 | 132 | ## 4.2.2 133 | 134 | - Added license 135 | - Dependabot security updates 136 | 137 | ## 4.2.1 138 | 139 | - Added more use cases to `no-return-wrap` 140 | 141 | ## 4.0.1 142 | 143 | - Remove `promise/param-names` fixer 144 | ([#146](https://github.com/eslint-community/eslint-plugin-promise/pull/146)) 145 | 146 | ## 4.0.0 147 | 148 | - Added fixer for `promise/no-new-statics` rule 149 | ([#133](https://github.com/eslint-community/eslint-plugin-promise/pull/133)) 150 | - Support ESLint v5 151 | ([#144](https://github.com/eslint-community/eslint-plugin-promise/pull/144)) 152 | 153 | This is a breaking change that drops support for Node v4. In order to use ESLint 154 | v5 and eslint-plugin-promise v4, you must use Node >=6. 155 | 156 | ## 3.8.0 157 | 158 | - Removed `promise/avoid-new` from recommended configuration 159 | ([#119](https://github.com/eslint-community/eslint-plugin-promise/pull/119)) 160 | - Ignored event listener callbacks in `promise/prefer-await-to-callbacks` 161 | ([#117](https://github.com/eslint-community/eslint-plugin-promise/pull/117)) 162 | - Ignored top-level awaits in `promise/prefer-await-to-then` 163 | ([#126](https://github.com/eslint-community/eslint-plugin-promise/pull/126)) 164 | - Added docs for `promise/no-nesting` and `promise/prefer-await-to-then` 165 | ([#120](https://github.com/eslint-community/eslint-plugin-promise/pull/120)) 166 | ([#121](https://github.com/eslint-community/eslint-plugin-promise/pull/121)) 167 | 168 | ## 3.7.0 169 | 170 | - Added `promise/valid-params` rule 171 | ([#85](https://github.com/eslint-community/eslint-plugin-promise/pull/85)) 172 | - Added `promise/no-new-statics` rule 173 | ([#82](https://github.com/eslint-community/eslint-plugin-promise/pull/82)) 174 | - Added fixer for `promise/param-names` rule 175 | ([#99](https://github.com/eslint-community/eslint-plugin-promise/pull/99)) 176 | - Added rule documentation to each rule 177 | ([#91](https://github.com/eslint-community/eslint-plugin-promise/pull/91)) 178 | 179 | ## 3.6.0 180 | 181 | - Added `['catch']` support in `catch-or-return` 182 | - Added `no-return-in-finally` rule 183 | - Fixed some formatting in the docs 184 | - Added `allowReject` option to `no-return-wrap` 185 | - Added exceptions for `no-callback-in-promise` 186 | 187 | ## 3.5.0 188 | 189 | - Added support for recommended settings using 190 | `extends: plugin:promise/recommended` 191 | 192 | ## 3.4.2 193 | 194 | - Fixed always return false positive with ternary (#31) 195 | 196 | ## 3.4.1 197 | 198 | - fixed #49 199 | 200 | ## 3.4.0 201 | 202 | - new rule: avoid-new 203 | - new rule: no-promise-in-callback 204 | - new rule: no-callback-in-promise 205 | - new rule: no-nesting 206 | 207 | ## 3.3.2 208 | 209 | - Removed eslint from peerDeps 210 | 211 | ## 3.3.1 212 | 213 | - Updated engines with proper stuff 214 | - Fixed bug for unreachable code 215 | 216 | ## 3.3.0 217 | 218 | - Rule: `prefer-async-to-callbacks` added 219 | - Rule: `prefer-async-to-then` added 220 | 221 | ## 3.2.1 222 | 223 | - Fix: `no-return-wrap` rule missing from index.js 224 | 225 | ## 3.2.0 226 | 227 | - Added `no-return-wrap` rule 228 | 229 | ## 3.1.0 230 | 231 | - Added multiple terminationMethods 232 | 233 | ## 3.0.1 234 | 235 | - Removed deprecated `always-catch` rule 236 | - FIX: always-return error with "fn && fn()" 237 | 238 | ## 3.0.0 239 | 240 | - Updated column and line numbers 241 | - Added flow analysis for better handling of if statements 242 | 243 | ## 2.0.1 244 | 245 | - Fixed type in docs 246 | 247 | ## 2.0.0 248 | 249 | - ESLint 3.0 Support 250 | 251 | ## 1.3.2 252 | 253 | - Updated tests to run on eslint 2.0 254 | - Fixed some issues with `no-native` rule 255 | 256 | ## 1.3.1 257 | 258 | - Actually added `no-native` rule 259 | 260 | ## 1.3.0 261 | 262 | - Added `no-native` rule 263 | 264 | ## 1.2.0 265 | 266 | - Allow `throw` in `always-return` rule 267 | - Added `terminationMethod` option to `catch-or-return` rule 268 | 269 | ## 1.1.0 270 | 271 | - Added `catch-or-return` rule 272 | 273 | ## 1.0.8 274 | 275 | - Fixed crash issues 276 | 277 | ## 1.0.0 - 1.0.7 278 | 279 | - Lots of basic feature updates and doc changes 280 | --------------------------------------------------------------------------------