eslint-plugin-promise
Advanced tools
Comparing version 3.8.0 to 6.1.1
170
CHANGELOG.md
@@ -0,35 +1,101 @@ | ||
## 6.0.2 | ||
- Added tests for @typescript-eslint/parser support | ||
## 6.0.1 | ||
- Fixed @typescript-eslint/parser issue #331, #205 | ||
## 6.0.0 | ||
- Dropped node 10 from engines #231 | ||
- Updated a ton of deps #236, #237, #235, #234 | ||
- ESLint 8 support #219 | ||
## 5.2.0 | ||
- Updated `param-names` rule to allow for unused params | ||
## 5.1.1 | ||
- Updated docs to include `no-callback-in-promise` reasons #215 | ||
## 5.1.0 | ||
- Included `catch()` and `finally()` in `prefer-await-to-then` #196 | ||
- Added some additional tests and upgraded some dev deps #196 | ||
- Exempted array methods in prefer-await-to-callbacks | ||
([#212](https://github.com/eslint-community/eslint-plugin-promise/issues/212)) | ||
## 5.0.0 | ||
- ESLint 7.0 Support | ||
## 4.3.1. | ||
- Updated and applied prettier | ||
## 4.3.0 | ||
- https://github.com/eslint-community/eslint-plugin-promise/pull/202 | ||
- Updated jest | ||
## 4.2.2 | ||
- Added license | ||
- Dependabot security updates | ||
## 4.2.1 | ||
- Added more use cases to `no-return-wrap` | ||
## 4.0.1 | ||
- Remove `promise/param-names` fixer | ||
([#146](https://github.com/eslint-community/eslint-plugin-promise/pull/146)) | ||
## 4.0.0 | ||
- Added fixer for `promise/no-new-statics` rule | ||
([#133](https://github.com/eslint-community/eslint-plugin-promise/pull/133)) | ||
- Support ESLint v5 | ||
([#144](https://github.com/eslint-community/eslint-plugin-promise/pull/144)) | ||
This is a breaking change that drops support for Node v4. In order to use ESLint | ||
v5 and eslint-plugin-promise v4, you must use Node >=6. | ||
## 3.8.0 | ||
* Removed `promise/avoid-new` from recommended configuration | ||
([#119](https://github.com/xjamundx/eslint-plugin-promise/pull/119)) | ||
* Ignored event listener callbacks in `promise/prefer-await-to-callbacks` | ||
([#117](https://github.com/xjamundx/eslint-plugin-promise/pull/117)) | ||
* Ignored top-level awaits in `promise/prefer-await-to-then` | ||
([#126](https://github.com/xjamundx/eslint-plugin-promise/pull/126)) | ||
* Added docs for `promise/no-nesting` and `promise/prefer-await-to-then` | ||
([#120](https://github.com/xjamundx/eslint-plugin-promise/pull/120)) | ||
([#121](https://github.com/xjamundx/eslint-plugin-promise/pull/121)) | ||
- Removed `promise/avoid-new` from recommended configuration | ||
([#119](https://github.com/eslint-community/eslint-plugin-promise/pull/119)) | ||
- Ignored event listener callbacks in `promise/prefer-await-to-callbacks` | ||
([#117](https://github.com/eslint-community/eslint-plugin-promise/pull/117)) | ||
- Ignored top-level awaits in `promise/prefer-await-to-then` | ||
([#126](https://github.com/eslint-community/eslint-plugin-promise/pull/126)) | ||
- Added docs for `promise/no-nesting` and `promise/prefer-await-to-then` | ||
([#120](https://github.com/eslint-community/eslint-plugin-promise/pull/120)) | ||
([#121](https://github.com/eslint-community/eslint-plugin-promise/pull/121)) | ||
## 3.7.0 | ||
* Added `promise/valid-params` rule | ||
([#85](https://github.com/xjamundx/eslint-plugin-promise/pull/85)) | ||
* Added `promise/no-new-statics` rule | ||
([#82](https://github.com/xjamundx/eslint-plugin-promise/pull/82)) | ||
* Added fixer for `promise/param-names` rule | ||
([#99](https://github.com/xjamundx/eslint-plugin-promise/pull/99)) | ||
* Added rule documentation to each rule | ||
([#91](https://github.com/xjamundx/eslint-plugin-promise/pull/91)) | ||
- Added `promise/valid-params` rule | ||
([#85](https://github.com/eslint-community/eslint-plugin-promise/pull/85)) | ||
- Added `promise/no-new-statics` rule | ||
([#82](https://github.com/eslint-community/eslint-plugin-promise/pull/82)) | ||
- Added fixer for `promise/param-names` rule | ||
([#99](https://github.com/eslint-community/eslint-plugin-promise/pull/99)) | ||
- Added rule documentation to each rule | ||
([#91](https://github.com/eslint-community/eslint-plugin-promise/pull/91)) | ||
## 3.6.0 | ||
* Added `['catch']` support in `catch-or-return` | ||
* Added `no-return-in-finally` rule | ||
* Fixed some formatting in the docs | ||
* Added `allowReject` option to `no-return-wrap` | ||
* Added exceptions for `no-callback-in-promise` | ||
- Added `['catch']` support in `catch-or-return` | ||
- Added `no-return-in-finally` rule | ||
- Fixed some formatting in the docs | ||
- Added `allowReject` option to `no-return-wrap` | ||
- Added exceptions for `no-callback-in-promise` | ||
## 3.5.0 | ||
* Added support for recommended settings using | ||
- Added support for recommended settings using | ||
`extends: plugin:promise/recommended` | ||
@@ -39,87 +105,87 @@ | ||
* Fixed always return false positive with ternary (#31) | ||
- Fixed always return false positive with ternary (#31) | ||
## 3.4.1 | ||
* fixed #49 | ||
- fixed #49 | ||
## 3.4.0 | ||
* new rule: avoid-new | ||
* new rule: no-promise-in-callback | ||
* new rule: no-callback-in-promise | ||
* new rule: no-nesting | ||
- new rule: avoid-new | ||
- new rule: no-promise-in-callback | ||
- new rule: no-callback-in-promise | ||
- new rule: no-nesting | ||
## 3.3.2 | ||
* Removed eslint from peerDeps | ||
- Removed eslint from peerDeps | ||
## 3.3.1 | ||
* Updated engines with proper stuff | ||
* Fixed bug for unreachable code | ||
- Updated engines with proper stuff | ||
- Fixed bug for unreachable code | ||
## 3.3.0 | ||
* Rule: `prefer-async-to-callbacks` added | ||
* Rule: `prefer-async-to-then` added | ||
- Rule: `prefer-async-to-callbacks` added | ||
- Rule: `prefer-async-to-then` added | ||
## 3.2.1 | ||
* Fix: `no-return-wrap` rule missing from index.js | ||
- Fix: `no-return-wrap` rule missing from index.js | ||
## 3.2.0 | ||
* Added `no-return-wrap` rule | ||
- Added `no-return-wrap` rule | ||
## 3.1.0 | ||
* Added multiple terminationMethods | ||
- Added multiple terminationMethods | ||
## 3.0.1 | ||
* Removed deprecated `always-catch` rule | ||
* FIX: always-return error with "fn && fn()" | ||
- Removed deprecated `always-catch` rule | ||
- FIX: always-return error with "fn && fn()" | ||
## 3.0.0 | ||
* Updated column and line numbers | ||
* Added flow analysis for better handling of if statements | ||
- Updated column and line numbers | ||
- Added flow analysis for better handling of if statements | ||
## 2.0.1 | ||
* Fixed type in docs | ||
- Fixed type in docs | ||
## 2.0.0 | ||
* ESLint 3.0 Support | ||
- ESLint 3.0 Support | ||
## 1.3.2 | ||
* Updated tests to run on eslint 2.0 | ||
* Fixed some issues with `no-native` rule | ||
- Updated tests to run on eslint 2.0 | ||
- Fixed some issues with `no-native` rule | ||
## 1.3.1 | ||
* Actually added `no-native` rule | ||
- Actually added `no-native` rule | ||
## 1.3.0 | ||
* Added `no-native` rule | ||
- Added `no-native` rule | ||
## 1.2.0 | ||
* Allow `throw` in `always-return` rule | ||
* Added `terminationMethod` option to `catch-or-return` rule | ||
- Allow `throw` in `always-return` rule | ||
- Added `terminationMethod` option to `catch-or-return` rule | ||
## 1.1.0 | ||
* Added `catch-or-return` rule | ||
- Added `catch-or-return` rule | ||
## 1.0.8 | ||
* Fixed crash issues | ||
- Fixed crash issues | ||
## 1.0.0 - 1.0.7 | ||
* Lots of basic feature updates and doc changes | ||
- Lots of basic feature updates and doc changes |
14
index.js
@@ -18,3 +18,4 @@ 'use strict' | ||
'no-return-in-finally': require('./rules/no-return-in-finally'), | ||
'valid-params': require('./rules/valid-params') | ||
'valid-params': require('./rules/valid-params'), | ||
'no-multiple-resolved': require('./rules/no-multiple-resolved'), | ||
}, | ||
@@ -26,6 +27,7 @@ rulesConfig: { | ||
'no-native': 0, | ||
'catch-or-return': 1 | ||
'catch-or-return': 1, | ||
}, | ||
configs: { | ||
recommended: { | ||
plugins: ['promise'], | ||
rules: { | ||
@@ -43,6 +45,6 @@ 'promise/always-return': 'error', | ||
'promise/no-return-in-finally': 'warn', | ||
'promise/valid-params': 'warn' | ||
} | ||
} | ||
} | ||
'promise/valid-params': 'warn', | ||
}, | ||
}, | ||
}, | ||
} |
108
package.json
{ | ||
"name": "eslint-plugin-promise", | ||
"version": "3.8.0", | ||
"version": "6.1.1", | ||
"description": "Enforce best practices for JavaScript promises", | ||
@@ -12,64 +12,68 @@ "keywords": [ | ||
], | ||
"homepage": "https://github.com/eslint-community/eslint-plugin-promise", | ||
"bugs": "https://github.com/eslint-community/eslint-plugin-promise/issues", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/eslint-community/eslint-plugin-promise" | ||
}, | ||
"license": "ISC", | ||
"author": "jden <jason@denizac.org>", | ||
"repository": "git@github.com:xjamundx/eslint-plugin-promise.git", | ||
"contributors": [ | ||
"Brett Zamir", | ||
"Aadit M Shah <aaditmshah@aadit.codes> (https://aadit.codes/)" | ||
], | ||
"scripts": { | ||
"precommit": "lint-staged", | ||
"test": "jest", | ||
"lint": "eslint index.js rules __tests__ --ignore-pattern '**/*.json'" | ||
"format": "prettier --write .", | ||
"lint": "eslint --report-unused-disable-directives .", | ||
"prepare": "husky install", | ||
"test": "jest --coverage" | ||
}, | ||
"devDependencies": { | ||
"doctoc": "^1.3.0", | ||
"eslint": "^4.17.0", | ||
"eslint-config-prettier": "^2.9.0", | ||
"eslint-config-standard": "^11.0.0-beta.0", | ||
"eslint-plugin-eslint-plugin": "^1.4.0", | ||
"eslint-plugin-import": "^2.8.0", | ||
"eslint-plugin-jest": "^21.12.2", | ||
"eslint-plugin-node": "^6.0.0", | ||
"eslint-plugin-prettier": "^2.6.0", | ||
"eslint-plugin-promise": "./", | ||
"eslint-plugin-standard": "^3.0.1", | ||
"husky": "^0.14.3", | ||
"jest": "^22.4.2", | ||
"jest-runner-eslint": "^0.4.0", | ||
"lint-staged": "^6.1.0", | ||
"prettier": "^1.10.2" | ||
}, | ||
"engines": { | ||
"node": ">=4" | ||
}, | ||
"license": "ISC", | ||
"lint-staged": { | ||
"concurrent": false, | ||
"linters": { | ||
"{README.md,CONTRIBUTING.md}": [ | ||
"doctoc --maxlevel 3 --notitle", | ||
"git add" | ||
], | ||
"*.js": ["prettier --write", "eslint --fix", "git add"], | ||
"*.+(json|md)": ["prettier --write", "git add"] | ||
} | ||
"{README.md,CONTRIBUTING.md}": [ | ||
"doctoc --maxlevel 3 --notitle" | ||
], | ||
"*.js": [ | ||
"prettier --write", | ||
"eslint --report-unused-disable-directives --fix" | ||
], | ||
"*.+(json|md)": [ | ||
"prettier --write" | ||
] | ||
}, | ||
"prettier": { | ||
"proseWrap": "always", | ||
"semi": false, | ||
"singleQuote": true, | ||
"proseWrap": "always" | ||
"singleQuote": true | ||
}, | ||
"jest": { | ||
"projects": [ | ||
{ | ||
"displayName": "test", | ||
"testEnvironment": "node" | ||
}, | ||
{ | ||
"runner": "jest-runner-eslint", | ||
"displayName": "lint", | ||
"testMatch": [ | ||
"<rootDir>/rules/**/*.js", | ||
"<rootDir>/__tests__/**/*.js", | ||
"<rootDir>/index.js" | ||
] | ||
"coverageThreshold": { | ||
"global": { | ||
"branches": 100, | ||
"functions": 100, | ||
"lines": 100, | ||
"statements": 100 | ||
} | ||
] | ||
} | ||
}, | ||
"devDependencies": { | ||
"@typescript-eslint/parser": "^5.40.0", | ||
"doctoc": "^2.2.1", | ||
"eslint": "^8.24.0", | ||
"eslint-config-prettier": "^8.5.0", | ||
"eslint-plugin-eslint-plugin": "^4.4.1", | ||
"eslint-plugin-jest": "^26.9.0", | ||
"eslint-plugin-node": "^11.1.0", | ||
"eslint-plugin-prettier": "^4.2.1", | ||
"husky": "^7.0.4", | ||
"jest": "^28.1.3", | ||
"lint-staged": "^12.5.0", | ||
"prettier": "^2.7.1", | ||
"typescript": "^4.8.4" | ||
}, | ||
"peerDependencies": { | ||
"eslint": "^7.0.0 || ^8.0.0" | ||
}, | ||
"engines": { | ||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" | ||
} | ||
} |
@@ -5,16 +5,14 @@ # eslint-plugin-promise | ||
[![travis-ci](https://travis-ci.org/xjamundx/eslint-plugin-promise.svg)](https://travis-ci.org/xjamundx/eslint-plugin-promise) | ||
[![CI](https://github.com/eslint-community/eslint-plugin-promise/actions/workflows/CI.yml/badge.svg)](https://github.com/eslint-community/eslint-plugin-promise/actions/workflows/CI.yml) | ||
[![npm version](https://badge.fury.io/js/eslint-plugin-promise.svg)](https://www.npmjs.com/package/eslint-plugin-promise) | ||
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) | ||
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) | ||
<!-- START doctoc generated TOC please keep comment here to allow auto update --> | ||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> | ||
* [Installation](#installation) | ||
* [Usage](#usage) | ||
* [Rules](#rules) | ||
* [Maintainers](#maintainers) | ||
* [License](#license) | ||
- [Installation](#installation) | ||
- [Usage](#usage) | ||
- [Rules](#rules) | ||
- [Maintainers](#maintainers) | ||
- [License](#license) | ||
@@ -42,4 +40,4 @@ <!-- END doctoc generated TOC please keep comment here to allow auto update --> | ||
Add `promise` to the plugins section of your `.eslintrc` configuration file. You | ||
can omit the `eslint-plugin-` prefix: | ||
Add `promise` to the plugins section of your `.eslintrc.json` configuration | ||
file. You can omit the `eslint-plugin-` prefix: | ||
@@ -73,3 +71,3 @@ ```json | ||
or start with the recommended rule set | ||
or start with the recommended rule set: | ||
@@ -88,3 +86,3 @@ ```json | ||
| [`no-return-wrap`][no-return-wrap] | Avoid wrapping values in `Promise.resolve` or `Promise.reject` when not needed. | :bangbang: | | | ||
| [`param-names`][param-names] | Enforce consistent param names when creating new promises. | :bangbang: | :wrench: | | ||
| [`param-names`][param-names] | Enforce consistent param names and ordering when creating new promises. | :bangbang: | | | ||
| [`always-return`][always-return] | Return inside each `then()` to create readable and reusable Promise chains. | :bangbang: | | | ||
@@ -96,7 +94,8 @@ | [`no-native`][no-native] | In an ES5 environment, make sure to create a `Promise` constructor before using. | | | | ||
| [`avoid-new`][avoid-new] | Avoid creating `new` promises outside of utility libs (use [pify][] instead) | | | | ||
| [`no-new-statics`][no-new-statics] | Avoid calling `new` on a Promise static method | :bangbang: | | | ||
| [`no-new-statics`][no-new-statics] | Avoid calling `new` on a Promise static method | :bangbang: | :wrench: | | ||
| [`no-return-in-finally`][no-return-in-finally] | Disallow return statements in `finally()` | :warning: | | | ||
| [`valid-params`][valid-params] | Ensures the proper number of arguments are passed to Promise functions | :warning: | | | ||
| [`prefer-await-to-then`][prefer-await-to-then] | Prefer `await` to `then()` for reading Promise values | :seven: | | | ||
| [`prefer-await-to-then`][prefer-await-to-then] | Prefer `await` to `then()`/`catch()`/`finally()` for reading Promise values | :seven: | | | ||
| [`prefer-await-to-callbacks`][prefer-await-to-callbacks] | Prefer async/await to the callback pattern | :seven: | | | ||
| [`no-multiple-resolved`][no-multiple-resolved] | Disallow creating new promises with paths that resolve multiple times | | | | ||
@@ -114,9 +113,10 @@ **Key** | ||
* Jamund Ferguson - [@xjamundx][] | ||
* Macklin Underdown - [@macklinu][] | ||
- Jamund Ferguson - [@xjamundx][] | ||
- Macklin Underdown - [@macklinu][] | ||
- Aadit M Shah - [@aaditmshah][] | ||
## License | ||
* (c) MMXV jden <mailto:jason@denizac.org> - ISC license. | ||
* (c) 2016 Jamund Ferguson <mailto:jamund@gmail.com> - ISC license. | ||
- (c) MMXV jden <mailto:jason@denizac.org> - ISC license. | ||
- (c) 2016 Jamund Ferguson <mailto:jamund@gmail.com> - ISC license. | ||
@@ -137,5 +137,7 @@ [catch-or-return]: docs/rules/catch-or-return.md | ||
[prefer-await-to-callbacks]: docs/rules/prefer-await-to-callbacks.md | ||
[no-multiple-resolved]: docs/rules/no-multiple-resolved.md | ||
[nodeify]: https://www.npmjs.com/package/nodeify | ||
[pify]: https://www.npmjs.com/package/pify | ||
[@aaditmshah]: https://github.com/aaditmshah | ||
[@macklinu]: https://github.com/macklinu | ||
[@xjamundx]: https://github.com/xjamundx |
@@ -5,2 +5,16 @@ 'use strict' | ||
/** | ||
* @typedef {import('estree').Node} Node | ||
* @typedef {import('estree').SimpleCallExpression} CallExpression | ||
* @typedef {import('estree').FunctionExpression} FunctionExpression | ||
* @typedef {import('estree').ArrowFunctionExpression} ArrowFunctionExpression | ||
* @typedef {import('eslint').Rule.CodePath} CodePath | ||
* @typedef {import('eslint').Rule.CodePathSegment} CodePathSegment | ||
*/ | ||
/** | ||
* @typedef { (FunctionExpression | ArrowFunctionExpression) & { parent: CallExpression }} InlineThenFunctionExpression | ||
*/ | ||
/** @param {Node} node */ | ||
function isFunctionWithBlockStatement(node) { | ||
@@ -16,12 +30,20 @@ if (node.type === 'FunctionExpression') { | ||
function isThenCallExpression(node) { | ||
/** | ||
* @param {string} memberName | ||
* @param {Node} node | ||
* @returns {node is CallExpression} | ||
*/ | ||
function isMemberCall(memberName, node) { | ||
return ( | ||
node.type === 'CallExpression' && | ||
node.callee.type === 'MemberExpression' && | ||
node.callee.property.name === 'then' | ||
!node.callee.computed && | ||
node.callee.property.type === 'Identifier' && | ||
node.callee.property.name === memberName | ||
) | ||
} | ||
/** @param {Node} node */ | ||
function isFirstArgument(node) { | ||
return ( | ||
return Boolean( | ||
node.parent && node.parent.arguments && node.parent.arguments[0] === node | ||
@@ -31,6 +53,10 @@ ) | ||
/** | ||
* @param {Node} node | ||
* @returns {node is InlineThenFunctionExpression} | ||
*/ | ||
function isInlineThenFunctionExpression(node) { | ||
return ( | ||
isFunctionWithBlockStatement(node) && | ||
isThenCallExpression(node.parent) && | ||
isMemberCall('then', node.parent) && | ||
isFirstArgument(node) | ||
@@ -40,18 +66,62 @@ ) | ||
function hasParentReturnStatement(node) { | ||
if (node && node.parent && node.parent.type) { | ||
// if the parent is a then, and we haven't returned anything, fail | ||
if (isThenCallExpression(node.parent)) { | ||
return false | ||
} | ||
if (node.parent.type === 'ReturnStatement') { | ||
/** | ||
* Checks whether the given node is the last `then()` callback in a promise chain. | ||
* @param {InlineThenFunctionExpression} node | ||
*/ | ||
function isLastCallback(node) { | ||
/** @type {Node} */ | ||
let target = node.parent | ||
/** @type {Node | undefined} */ | ||
let parent = target.parent | ||
while (parent) { | ||
if (parent.type === 'ExpressionStatement') { | ||
// e.g. { promise.then(() => value) } | ||
return true | ||
} | ||
return hasParentReturnStatement(node.parent) | ||
if (parent.type === 'UnaryExpression') { | ||
// e.g. void promise.then(() => value) | ||
return parent.operator === 'void' | ||
} | ||
/** @type {Node | null} */ | ||
let nextTarget = null | ||
if (parent.type === 'SequenceExpression') { | ||
if (peek(parent.expressions) !== target) { | ||
// e.g. (promise?.then(() => value), expr) | ||
return true | ||
} | ||
nextTarget = parent | ||
} else if ( | ||
// e.g. promise?.then(() => value) | ||
parent.type === 'ChainExpression' || | ||
// e.g. await promise.then(() => value) | ||
parent.type === 'AwaitExpression' | ||
) { | ||
nextTarget = parent | ||
} else if (parent.type === 'MemberExpression') { | ||
if ( | ||
parent.parent && | ||
(isMemberCall('catch', parent.parent) || | ||
isMemberCall('finally', parent.parent)) | ||
) { | ||
// e.g. promise.then(() => value).catch(e => {}) | ||
nextTarget = parent.parent | ||
} | ||
} | ||
if (nextTarget) { | ||
target = nextTarget | ||
parent = target.parent | ||
continue | ||
} | ||
return false | ||
} | ||
// istanbul ignore next | ||
return false | ||
} | ||
/** | ||
* @template T | ||
* @param {T[]} arr | ||
* @returns {T} | ||
*/ | ||
function peek(arr) { | ||
@@ -63,36 +133,59 @@ return arr[arr.length - 1] | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
url: getDocsUrl('always-return') | ||
} | ||
url: getDocsUrl('always-return'), | ||
}, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
ignoreLastCallback: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
create: function(context) { | ||
// funcInfoStack is a stack representing the stack of currently executing | ||
// functions | ||
// funcInfoStack[i].branchIDStack is a stack representing the currently | ||
// executing branches ("codePathSegment"s) within the given function | ||
// funcInfoStack[i].branchInfoMap is an object representing information | ||
// about all branches within the given function | ||
// funcInfoStack[i].branchInfoMap[j].good is a boolean representing whether | ||
// the given branch explictly `return`s or `throw`s. It starts as `false` | ||
// for every branch and is updated to `true` if a `return` or `throw` | ||
// statement is found | ||
// funcInfoStack[i].branchInfoMap[j].loc is a eslint SourceLocation object | ||
// for the given branch | ||
// example: | ||
// funcInfoStack = [ { branchIDStack: [ 's1_1' ], | ||
// branchInfoMap: | ||
// { s1_1: | ||
// { good: false, | ||
// loc: <loc> } } }, | ||
// { branchIDStack: ['s2_1', 's2_4'], | ||
// branchInfoMap: | ||
// { s2_1: | ||
// { good: false, | ||
// loc: <loc> }, | ||
// s2_2: | ||
// { good: true, | ||
// loc: <loc> }, | ||
// s2_4: | ||
// { good: false, | ||
// loc: <loc> } } } ] | ||
create(context) { | ||
const options = context.options[0] || {} | ||
const ignoreLastCallback = !!options.ignoreLastCallback | ||
/** | ||
* @typedef {object} FuncInfo | ||
* @property {string[]} branchIDStack This is a stack representing the currently | ||
* executing branches ("codePathSegment"s) within the given function | ||
* @property {Record<string, BranchInfo | undefined>} branchInfoMap This is an object representing information | ||
* about all branches within the given function | ||
* | ||
* @typedef {object} BranchInfo | ||
* @property {boolean} good This is a boolean representing whether | ||
* the given branch explicitly `return`s or `throw`s. It starts as `false` | ||
* for every branch and is updated to `true` if a `return` or `throw` | ||
* statement is found | ||
* @property {Node} node This is a estree Node object | ||
* for the given branch | ||
*/ | ||
/** | ||
* funcInfoStack is a stack representing the stack of currently executing | ||
* functions | ||
* example: | ||
* funcInfoStack = [ { branchIDStack: [ 's1_1' ], | ||
* branchInfoMap: | ||
* { s1_1: | ||
* { good: false, | ||
* loc: <loc> } } }, | ||
* { branchIDStack: ['s2_1', 's2_4'], | ||
* branchInfoMap: | ||
* { s2_1: | ||
* { good: false, | ||
* loc: <loc> }, | ||
* s2_2: | ||
* { good: true, | ||
* loc: <loc> }, | ||
* s2_4: | ||
* { good: false, | ||
* loc: <loc> } } } ] | ||
* @type {FuncInfo[]} | ||
*/ | ||
const funcInfoStack = [] | ||
@@ -110,12 +203,16 @@ | ||
return { | ||
ReturnStatement: markCurrentBranchAsGood, | ||
ThrowStatement: markCurrentBranchAsGood, | ||
'ReturnStatement:exit': markCurrentBranchAsGood, | ||
'ThrowStatement:exit': markCurrentBranchAsGood, | ||
onCodePathSegmentStart: function(segment, node) { | ||
/** | ||
* @param {CodePathSegment} segment | ||
* @param {Node} node | ||
*/ | ||
onCodePathSegmentStart(segment, node) { | ||
const funcInfo = peek(funcInfoStack) | ||
funcInfo.branchIDStack.push(segment.id) | ||
funcInfo.branchInfoMap[segment.id] = { good: false, node: node } | ||
funcInfo.branchInfoMap[segment.id] = { good: false, node } | ||
}, | ||
onCodePathSegmentEnd: function(segment, node) { | ||
onCodePathSegmentEnd() { | ||
const funcInfo = peek(funcInfoStack) | ||
@@ -125,10 +222,14 @@ funcInfo.branchIDStack.pop() | ||
onCodePathStart: function(path, node) { | ||
onCodePathStart() { | ||
funcInfoStack.push({ | ||
branchIDStack: [], | ||
branchInfoMap: {} | ||
branchInfoMap: {}, | ||
}) | ||
}, | ||
onCodePathEnd: function(path, node) { | ||
/** | ||
* @param {CodePath} path | ||
* @param {Node} node | ||
*/ | ||
onCodePathEnd(path, node) { | ||
const funcInfo = funcInfoStack.pop() | ||
@@ -140,26 +241,19 @@ | ||
path.finalSegments.forEach(segment => { | ||
if (ignoreLastCallback && isLastCallback(node)) { | ||
return | ||
} | ||
path.finalSegments.forEach((segment) => { | ||
const id = segment.id | ||
const branch = funcInfo.branchInfoMap[id] | ||
if (!branch.good) { | ||
if (hasParentReturnStatement(branch.node)) { | ||
return | ||
} | ||
// check shortcircuit syntax like `x && x()` and `y || x()`` | ||
const prevSegments = segment.prevSegments | ||
for (let ii = prevSegments.length - 1; ii >= 0; --ii) { | ||
const prevSegment = prevSegments[ii] | ||
if (funcInfo.branchInfoMap[prevSegment.id].good) return | ||
} | ||
context.report({ | ||
message: 'Each then() should return a value or throw', | ||
node: branch.node | ||
node: branch.node, | ||
}) | ||
} | ||
}) | ||
} | ||
}, | ||
} | ||
} | ||
}, | ||
} |
@@ -12,15 +12,17 @@ /** | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: getDocsUrl('avoid-new') | ||
} | ||
url: getDocsUrl('avoid-new'), | ||
}, | ||
schema: [], | ||
}, | ||
create: function(context) { | ||
create(context) { | ||
return { | ||
NewExpression: function(node) { | ||
NewExpression(node) { | ||
if (node.callee.name === 'Promise') { | ||
context.report({ node, message: 'Avoid creating new promises.' }) | ||
} | ||
} | ||
}, | ||
} | ||
} | ||
}, | ||
} |
@@ -14,9 +14,39 @@ /** | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
url: getDocsUrl('catch-or-return') | ||
} | ||
url: getDocsUrl('catch-or-return'), | ||
}, | ||
messages: { | ||
terminationMethod: 'Expected {{ terminationMethod }}() or return', | ||
}, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
allowFinally: { | ||
type: 'boolean', | ||
}, | ||
allowThen: { | ||
type: 'boolean', | ||
}, | ||
terminationMethod: { | ||
oneOf: [ | ||
{ type: 'string' }, | ||
{ | ||
type: 'array', | ||
items: { | ||
type: 'string', | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
create: function(context) { | ||
create(context) { | ||
const options = context.options[0] || {} | ||
const allowThen = options.allowThen | ||
const allowFinally = options.allowFinally | ||
let terminationMethod = options.terminationMethod || 'catch' | ||
@@ -28,4 +58,50 @@ | ||
function isAllowedPromiseTermination(expression) { | ||
// somePromise.then(a, b) | ||
if ( | ||
allowThen && | ||
expression.type === 'CallExpression' && | ||
expression.callee.type === 'MemberExpression' && | ||
expression.callee.property.name === 'then' && | ||
expression.arguments.length === 2 | ||
) { | ||
return true | ||
} | ||
// somePromise.catch().finally(fn) | ||
if ( | ||
allowFinally && | ||
expression.type === 'CallExpression' && | ||
expression.callee.type === 'MemberExpression' && | ||
expression.callee.property.name === 'finally' && | ||
isPromise(expression.callee.object) && | ||
isAllowedPromiseTermination(expression.callee.object) | ||
) { | ||
return true | ||
} | ||
// somePromise.catch() | ||
if ( | ||
expression.type === 'CallExpression' && | ||
expression.callee.type === 'MemberExpression' && | ||
terminationMethod.indexOf(expression.callee.property.name) !== -1 | ||
) { | ||
return true | ||
} | ||
// somePromise['catch']() | ||
if ( | ||
expression.type === 'CallExpression' && | ||
expression.callee.type === 'MemberExpression' && | ||
expression.callee.property.type === 'Literal' && | ||
expression.callee.property.value === 'catch' | ||
) { | ||
return true | ||
} | ||
return false | ||
} | ||
return { | ||
ExpressionStatement: function(node) { | ||
ExpressionStatement(node) { | ||
if (!isPromise(node.expression)) { | ||
@@ -35,40 +111,14 @@ return | ||
// somePromise.then(a, b) | ||
if ( | ||
allowThen && | ||
node.expression.type === 'CallExpression' && | ||
node.expression.callee.type === 'MemberExpression' && | ||
node.expression.callee.property.name === 'then' && | ||
node.expression.arguments.length === 2 | ||
) { | ||
if (isAllowedPromiseTermination(node.expression)) { | ||
return | ||
} | ||
// somePromise.catch() | ||
if ( | ||
node.expression.type === 'CallExpression' && | ||
node.expression.callee.type === 'MemberExpression' && | ||
terminationMethod.indexOf(node.expression.callee.property.name) !== -1 | ||
) { | ||
return | ||
} | ||
// somePromise['catch']() | ||
if ( | ||
node.expression.type === 'CallExpression' && | ||
node.expression.callee.type === 'MemberExpression' && | ||
node.expression.callee.property.type === 'Literal' && | ||
node.expression.callee.property.value === 'catch' | ||
) { | ||
return | ||
} | ||
context.report({ | ||
node, | ||
message: 'Expected {{ terminationMethod }}() or return', | ||
data: { terminationMethod } | ||
messageId: 'terminationMethod', | ||
data: { terminationMethod }, | ||
}) | ||
} | ||
}, | ||
} | ||
} | ||
}, | ||
} |
'use strict' | ||
const pkg = require('../../package') | ||
const REPO_URL = 'https://github.com/eslint-community/eslint-plugin-promise' | ||
const REPO_URL = 'https://github.com/xjamundx/eslint-plugin-promise' | ||
/** | ||
@@ -16,5 +14,5 @@ * Generates the URL to documentation for the given rule name. It uses the | ||
function getDocsUrl(ruleName) { | ||
return `${REPO_URL}/tree/v${pkg.version}/docs/rules/${ruleName}.md` | ||
return `${REPO_URL}/blob/main/docs/rules/${ruleName}.md` | ||
} | ||
module.exports = getDocsUrl |
/** | ||
* Library: Has Promis eCallback | ||
* Library: Has Promise Callback | ||
* Makes sure that an Expression node is part of a promise | ||
@@ -9,3 +9,24 @@ * with callback functions (like then() or catch()) | ||
/** | ||
* @typedef {import('estree').SimpleCallExpression} CallExpression | ||
* @typedef {import('estree').MemberExpression} MemberExpression | ||
* @typedef {import('estree').Identifier} Identifier | ||
* | ||
* @typedef {object} NameIsThenOrCatch | ||
* @property {'then' | 'catch'} name | ||
* | ||
* @typedef {object} PropertyIsThenOrCatch | ||
* @property {Identifier & NameIsThenOrCatch} property | ||
* | ||
* @typedef {object} CalleeIsPromiseCallback | ||
* @property {MemberExpression & PropertyIsThenOrCatch} callee | ||
* | ||
* @typedef {CallExpression & CalleeIsPromiseCallback} HasPromiseCallback | ||
*/ | ||
/** | ||
* @param {import('estree').Node} node | ||
* @returns {node is HasPromiseCallback} | ||
*/ | ||
function hasPromiseCallback(node) { | ||
// istanbul ignore if -- only being called within `CallExpression` | ||
if (node.type !== 'CallExpression') return | ||
@@ -12,0 +33,0 @@ if (node.callee.type !== 'MemberExpression') return |
@@ -5,4 +5,5 @@ 'use strict' | ||
function isCallingBack(node, exceptions) { | ||
function isCallback(node, exceptions) { | ||
const isCallExpression = node.type === 'CallExpression' | ||
// istanbul ignore next -- always invoked on `CallExpression` | ||
const callee = node.callee || {} | ||
@@ -14,2 +15,2 @@ const nameIsCallback = isNamedCallback(callee.name, exceptions) | ||
module.exports = isCallingBack | ||
module.exports = isCallback |
@@ -7,9 +7,9 @@ 'use strict' | ||
for (let i = 0; i < exceptions.length; i++) { | ||
callbacks = callbacks.filter(function(item) { | ||
callbacks = callbacks.filter((item) => { | ||
return item !== exceptions[i] | ||
}) | ||
} | ||
return callbacks.some(function(trueCallbackName) { | ||
return callbacks.some((trueCallbackName) => { | ||
return potentialCallbackName === trueCallbackName | ||
}) | ||
} |
@@ -5,5 +5,7 @@ 'use strict' | ||
all: true, | ||
allSettled: true, | ||
any: true, | ||
race: true, | ||
reject: true, | ||
resolve: true | ||
resolve: true, | ||
} |
@@ -15,9 +15,27 @@ /** | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: getDocsUrl('no-callback-in-promise') | ||
} | ||
url: getDocsUrl('no-callback-in-promise'), | ||
}, | ||
messages: { | ||
callback: 'Avoid calling back inside of a promise.', | ||
}, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
exceptions: { | ||
type: 'array', | ||
items: { | ||
type: 'string', | ||
}, | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
create: function(context) { | ||
create(context) { | ||
return { | ||
CallExpression: function(node) { | ||
CallExpression(node) { | ||
const options = context.options[0] || {} | ||
@@ -39,3 +57,3 @@ const exceptions = options.exceptions || [] | ||
node: node.arguments[0], | ||
message: 'Avoid calling back inside of a promise.' | ||
messageId: 'callback', | ||
}) | ||
@@ -49,8 +67,8 @@ } | ||
node, | ||
message: 'Avoid calling back inside of a promise.' | ||
messageId: 'callback', | ||
}) | ||
} | ||
} | ||
}, | ||
} | ||
} | ||
}, | ||
} |
@@ -9,3 +9,3 @@ // Borrowed from here: | ||
function isDeclared(scope, ref) { | ||
return scope.variables.some(function(variable) { | ||
return scope.variables.some((variable) => { | ||
if (variable.name !== ref.identifier.name) { | ||
@@ -15,2 +15,5 @@ return false | ||
// Presumably can't pass this since the implicit `Promise` global | ||
// being checked here would always lack `defs` | ||
// istanbul ignore else | ||
if (!variable.defs || !variable.defs.length) { | ||
@@ -20,2 +23,3 @@ return false | ||
// istanbul ignore next | ||
return true | ||
@@ -27,13 +31,16 @@ }) | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: getDocsUrl('no-native') | ||
} | ||
url: getDocsUrl('no-native'), | ||
}, | ||
messages: { | ||
name: '"{{name}}" is not defined.', | ||
}, | ||
schema: [], | ||
}, | ||
create: function(context) { | ||
const MESSAGE = '"{{name}}" is not defined.' | ||
create(context) { | ||
/** | ||
* Checks for and reports reassigned constants | ||
* | ||
* @param {Scope} scope - an escope Scope object | ||
* @param {Scope} scope - an eslint-scope Scope object | ||
* @returns {void} | ||
@@ -43,6 +50,14 @@ * @private | ||
return { | ||
'Program:exit': function() { | ||
'Program:exit'() { | ||
const scope = context.getScope() | ||
const leftToBeResolved = | ||
scope.implicit.left || | ||
/** | ||
* Fixes https://github.com/eslint-community/eslint-plugin-promise/issues/205. | ||
* The problem was that @typescript-eslint has a scope manager | ||
* which has `leftToBeResolved` instead of the default `left`. | ||
*/ | ||
scope.implicit.leftToBeResolved | ||
scope.implicit.left.forEach(function(ref) { | ||
leftToBeResolved.forEach((ref) => { | ||
if (ref.identifier.name !== 'Promise') { | ||
@@ -52,13 +67,14 @@ return | ||
// istanbul ignore else | ||
if (!isDeclared(scope, ref)) { | ||
context.report({ | ||
node: ref.identifier, | ||
message: MESSAGE, | ||
data: { name: ref.identifier.name } | ||
messageId: 'name', | ||
data: { name: ref.identifier.name }, | ||
}) | ||
} | ||
}) | ||
} | ||
}, | ||
} | ||
} | ||
}, | ||
} |
@@ -14,16 +14,106 @@ /** | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: getDocsUrl('no-nesting') | ||
url: getDocsUrl('no-nesting'), | ||
}, | ||
schema: [], | ||
}, | ||
create(context) { | ||
/** | ||
* Array of callback function scopes. | ||
* Scopes are in order closest to the current node. | ||
* @type {import('eslint').Scope.Scope[]} | ||
*/ | ||
const callbackScopes = [] | ||
/** | ||
* @param {import('eslint').Scope.Scope} scope | ||
* @returns {Iterable<import('eslint').Scope.Reference>} | ||
*/ | ||
function* iterateDefinedReferences(scope) { | ||
for (const variable of scope.variables) { | ||
for (const reference of variable.references) { | ||
yield reference | ||
} | ||
} | ||
} | ||
}, | ||
create: function(context) { | ||
return { | ||
CallExpression: function(node) { | ||
':function'(node) { | ||
if (isInsidePromise(node)) { | ||
callbackScopes.unshift(context.getScope()) | ||
} | ||
}, | ||
':function:exit'(node) { | ||
if (isInsidePromise(node)) { | ||
callbackScopes.shift() | ||
} | ||
}, | ||
CallExpression(node) { | ||
if (!hasPromiseCallback(node)) return | ||
if (context.getAncestors().some(isInsidePromise)) { | ||
context.report({ node, message: 'Avoid nesting promises.' }) | ||
if (!callbackScopes.length) { | ||
// The node is not in the callback function. | ||
return | ||
} | ||
} | ||
// Checks if the argument callback uses variables defined in the closest callback function scope. | ||
// | ||
// e.g. | ||
// ``` | ||
// doThing() | ||
// .then(a => getB(a) | ||
// .then(b => getC(a, b)) | ||
// ) | ||
// ``` | ||
// | ||
// In the above case, Since the variables it references are undef, | ||
// we cannot refactor the nesting like following: | ||
// ``` | ||
// doThing() | ||
// .then(a => getB(a)) | ||
// .then(b => getC(a, b)) | ||
// ``` | ||
// | ||
// However, `getD` can be refactored in the following: | ||
// ``` | ||
// doThing() | ||
// .then(a => getB(a) | ||
// .then(b => getC(a, b) | ||
// .then(c => getD(a, c)) | ||
// ) | ||
// ) | ||
// ``` | ||
// ↓ | ||
// ``` | ||
// doThing() | ||
// .then(a => getB(a) | ||
// .then(b => getC(a, b)) | ||
// .then(c => getD(a, c)) | ||
// ) | ||
// ``` | ||
// This is why we only check the closest callback function scope. | ||
// | ||
const closestCallbackScope = callbackScopes[0] | ||
for (const reference of iterateDefinedReferences( | ||
closestCallbackScope | ||
)) { | ||
if ( | ||
node.arguments.some( | ||
(arg) => | ||
arg.range[0] <= reference.identifier.range[0] && | ||
reference.identifier.range[1] <= arg.range[1] | ||
) | ||
) { | ||
// Argument callbacks refer to variables defined in the callback function. | ||
return | ||
} | ||
} | ||
context.report({ | ||
node: node.callee.property, | ||
message: 'Avoid nesting promises.', | ||
}) | ||
}, | ||
} | ||
} | ||
}, | ||
} |
@@ -8,5 +8,8 @@ 'use strict' | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
url: getDocsUrl('no-new-statics') | ||
} | ||
url: getDocsUrl('no-new-statics'), | ||
}, | ||
fixable: 'code', | ||
schema: [], | ||
}, | ||
@@ -24,8 +27,14 @@ create(context) { | ||
message: "Avoid calling 'new' on 'Promise.{{ name }}()'", | ||
data: { name: node.callee.property.name } | ||
data: { name: node.callee.property.name }, | ||
fix(fixer) { | ||
return fixer.replaceTextRange( | ||
[node.range[0], node.range[0] + 'new '.length], | ||
'' | ||
) | ||
}, | ||
}) | ||
} | ||
} | ||
}, | ||
} | ||
} | ||
}, | ||
} |
@@ -14,9 +14,11 @@ /** | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: getDocsUrl('no-promise-in-callback') | ||
} | ||
url: getDocsUrl('no-promise-in-callback'), | ||
}, | ||
schema: [], | ||
}, | ||
create: function(context) { | ||
create(context) { | ||
return { | ||
CallExpression: function(node) { | ||
CallExpression(node) { | ||
if (!isPromise(node)) return | ||
@@ -34,8 +36,8 @@ | ||
node: node.callee, | ||
message: 'Avoid using promises inside of callbacks.' | ||
message: 'Avoid using promises inside of callbacks.', | ||
}) | ||
} | ||
} | ||
}, | ||
} | ||
} | ||
}, | ||
} |
@@ -8,9 +8,11 @@ 'use strict' | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
url: getDocsUrl('no-return-in-finally') | ||
} | ||
url: getDocsUrl('no-return-in-finally'), | ||
}, | ||
schema: [], | ||
}, | ||
create: function(context) { | ||
create(context) { | ||
return { | ||
CallExpression: function(node) { | ||
CallExpression(node) { | ||
if (isPromise(node)) { | ||
@@ -22,2 +24,3 @@ if ( | ||
) { | ||
// istanbul ignore else -- passing `isPromise` means should have a body | ||
if ( | ||
@@ -30,3 +33,3 @@ node.arguments && | ||
if ( | ||
node.arguments[0].body.body.some(function(statement) { | ||
node.arguments[0].body.body.some((statement) => { | ||
return statement.type === 'ReturnStatement' | ||
@@ -37,3 +40,3 @@ }) | ||
node: node.callee.property, | ||
message: 'No return in finally' | ||
message: 'No return in finally', | ||
}) | ||
@@ -44,5 +47,5 @@ } | ||
} | ||
} | ||
}, | ||
} | ||
} | ||
}, | ||
} |
/** | ||
* Rule: no-return-wrap function | ||
* Prevents uneccessary wrapping of results in Promise.resolve | ||
* Prevents unnecessary wrapping of results in Promise.resolve | ||
* or Promise.reject as the Promise will do that for us | ||
@@ -11,10 +11,27 @@ */ | ||
const isPromise = require('./lib/is-promise') | ||
const rejectMessage = 'Expected throw instead of Promise.reject' | ||
const resolveMessage = 'Avoid wrapping return values in Promise.resolve' | ||
function isInPromise(context) { | ||
const expression = context.getAncestors().filter(function(node) { | ||
return node.type === 'ExpressionStatement' | ||
})[0] | ||
return expression && expression.expression && isPromise(expression.expression) | ||
let functionNode = context | ||
.getAncestors() | ||
.filter((node) => { | ||
return ( | ||
node.type === 'ArrowFunctionExpression' || | ||
node.type === 'FunctionExpression' | ||
) | ||
}) | ||
.reverse()[0] | ||
while ( | ||
functionNode && | ||
functionNode.parent && | ||
functionNode.parent.type === 'MemberExpression' && | ||
functionNode.parent.object === functionNode && | ||
functionNode.parent.property.type === 'Identifier' && | ||
functionNode.parent.property.name === 'bind' && | ||
functionNode.parent.parent && | ||
functionNode.parent.parent.type === 'CallExpression' && | ||
functionNode.parent.parent.callee === functionNode.parent | ||
) { | ||
functionNode = functionNode.parent.parent | ||
} | ||
return functionNode && functionNode.parent && isPromise(functionNode.parent) | ||
} | ||
@@ -24,33 +41,56 @@ | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: getDocsUrl('no-return-wrap') | ||
} | ||
url: getDocsUrl('no-return-wrap'), | ||
}, | ||
messages: { | ||
resolve: 'Avoid wrapping return values in Promise.resolve', | ||
reject: 'Expected throw instead of Promise.reject', | ||
}, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
allowReject: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
create: function(context) { | ||
create(context) { | ||
const options = context.options[0] || {} | ||
const allowReject = options.allowReject | ||
return { | ||
ReturnStatement: function(node) { | ||
if (isInPromise(context)) { | ||
if (node.argument) { | ||
if (node.argument.type === 'CallExpression') { | ||
if (node.argument.callee.type === 'MemberExpression') { | ||
if (node.argument.callee.object.name === 'Promise') { | ||
if (node.argument.callee.property.name === 'resolve') { | ||
context.report({ node, message: resolveMessage }) | ||
} else if ( | ||
!allowReject && | ||
node.argument.callee.property.name === 'reject' | ||
) { | ||
context.report({ node, message: rejectMessage }) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Checks a call expression, reporting if necessary. | ||
* @param callExpression The call expression. | ||
* @param node The node to report. | ||
*/ | ||
function checkCallExpression({ callee }, node) { | ||
if ( | ||
isInPromise(context) && | ||
callee.type === 'MemberExpression' && | ||
callee.object.name === 'Promise' | ||
) { | ||
if (callee.property.name === 'resolve') { | ||
context.report({ node, messageId: 'resolve' }) | ||
} else if (!allowReject && callee.property.name === 'reject') { | ||
context.report({ node, messageId: 'reject' }) | ||
} | ||
} | ||
} | ||
} | ||
return { | ||
ReturnStatement(node) { | ||
if (node.argument && node.argument.type === 'CallExpression') { | ||
checkCallExpression(node.argument, node) | ||
} | ||
}, | ||
'ArrowFunctionExpression > CallExpression'(node) { | ||
checkCallExpression(node, node) | ||
}, | ||
} | ||
}, | ||
} |
'use strict' | ||
const getDocsUrl = require('./lib/get-docs-url') | ||
const { | ||
isPromiseConstructorWithInlineExecutor, | ||
} = require('./lib/is-promise-constructor') | ||
module.exports = { | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: getDocsUrl('param-names') | ||
url: getDocsUrl('param-names'), | ||
}, | ||
fixable: 'code' | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
resolvePattern: { type: 'string' }, | ||
rejectPattern: { type: 'string' }, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
create(context) { | ||
const options = context.options[0] || {} | ||
const resolvePattern = new RegExp( | ||
options.resolvePattern || '^_?resolve$', | ||
'u' | ||
) | ||
const rejectPattern = new RegExp(options.rejectPattern || '^_?reject$', 'u') | ||
return { | ||
NewExpression(node) { | ||
if (node.callee.name === 'Promise' && node.arguments.length === 1) { | ||
if (isPromiseConstructorWithInlineExecutor(node)) { | ||
const params = node.arguments[0].params | ||
@@ -22,22 +42,28 @@ | ||
if ( | ||
params[0].name !== 'resolve' || | ||
(params[1] && params[1].name !== 'reject') | ||
) { | ||
const resolveParamName = params[0] && params[0].name | ||
if (resolveParamName && !resolvePattern.test(resolveParamName)) { | ||
context.report({ | ||
node, | ||
node: params[0], | ||
message: | ||
'Promise constructor parameters must be named resolve, reject', | ||
fix(fixer) { | ||
return [ | ||
fixer.replaceText(params[0], 'resolve'), | ||
params[1] && fixer.replaceText(params[1], 'reject') | ||
].filter(Boolean) | ||
} | ||
'Promise constructor parameters must be named to match "{{ resolvePattern }}"', | ||
data: { | ||
resolvePattern: resolvePattern.source, | ||
}, | ||
}) | ||
} | ||
const rejectParamName = params[1] && params[1].name | ||
if (rejectParamName && !rejectPattern.test(rejectParamName)) { | ||
context.report({ | ||
node: params[1], | ||
message: | ||
'Promise constructor parameters must be named to match "{{ rejectPattern }}"', | ||
data: { | ||
rejectPattern: rejectPattern.source, | ||
}, | ||
}) | ||
} | ||
} | ||
} | ||
}, | ||
} | ||
} | ||
}, | ||
} |
@@ -5,9 +5,12 @@ 'use strict' | ||
const errorMessage = 'Avoid callbacks. Prefer Async/Await.' | ||
module.exports = { | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: getDocsUrl('prefer-await-to-callbacks') | ||
} | ||
url: getDocsUrl('prefer-await-to-callbacks'), | ||
}, | ||
messages: { | ||
error: 'Avoid callbacks. Prefer Async/Await.', | ||
}, | ||
schema: [], | ||
}, | ||
@@ -18,7 +21,7 @@ create(context) { | ||
if (lastParam.name === 'callback' || lastParam.name === 'cb') { | ||
context.report({ node: lastParam, message: errorMessage }) | ||
context.report({ node: lastParam, messageId: 'error' }) | ||
} | ||
} | ||
function isInsideYieldOrAwait() { | ||
return context.getAncestors().some(parent => { | ||
return context.getAncestors().some((parent) => { | ||
return ( | ||
@@ -33,3 +36,3 @@ parent.type === 'AwaitExpression' || parent.type === 'YieldExpression' | ||
if (node.callee.name === 'cb' || node.callee.name === 'callback') { | ||
context.report({ node, message: errorMessage }) | ||
context.report({ node, messageId: 'error' }) | ||
return | ||
@@ -54,5 +57,34 @@ } | ||
} | ||
if (arg.params && arg.params[0] && arg.params[0].name === 'err') { | ||
// carve out exemption for map/filter/etc | ||
const arrayMethods = [ | ||
'map', | ||
'every', | ||
'forEach', | ||
'some', | ||
'find', | ||
'filter', | ||
] | ||
const isLodash = | ||
node.callee.object && | ||
['lodash', 'underscore', '_'].includes(node.callee.object.name) | ||
const callsArrayMethod = | ||
node.callee.property && | ||
arrayMethods.includes(node.callee.property.name) && | ||
(node.arguments.length === 1 || | ||
(node.arguments.length === 2 && isLodash)) | ||
const isArrayMethod = | ||
node.callee.name && | ||
arrayMethods.includes(node.callee.name) && | ||
node.arguments.length === 2 | ||
if (callsArrayMethod || isArrayMethod) return | ||
// actually check for callbacks (I know this is the worst) | ||
if ( | ||
arg.params && | ||
arg.params[0] && | ||
(arg.params[0].name === 'err' || arg.params[0].name === 'error') | ||
) { | ||
if (!isInsideYieldOrAwait()) { | ||
context.report({ node: arg, message: errorMessage }) | ||
context.report({ node: arg, messageId: 'error' }) | ||
} | ||
@@ -64,5 +96,5 @@ } | ||
FunctionExpression: checkLastParamsForCallback, | ||
ArrowFunctionExpression: checkLastParamsForCallback | ||
ArrowFunctionExpression: checkLastParamsForCallback, | ||
} | ||
} | ||
}, | ||
} |
/** | ||
* Rule: prefer-await-to-then | ||
* Discourage using then() and instead use async/await. | ||
* Discourage using then()/catch()/finally() and instead use async/await. | ||
*/ | ||
@@ -12,10 +12,12 @@ | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: getDocsUrl('prefer-await-to-then') | ||
} | ||
url: getDocsUrl('prefer-await-to-then'), | ||
}, | ||
schema: [], | ||
}, | ||
create: function(context) { | ||
create(context) { | ||
/** Returns true if node is inside yield or await expression. */ | ||
function isInsideYieldOrAwait() { | ||
return context.getAncestors().some(parent => { | ||
return context.getAncestors().some((parent) => { | ||
return ( | ||
@@ -37,3 +39,3 @@ parent.type === 'AwaitExpression' || parent.type === 'YieldExpression' | ||
return { | ||
MemberExpression: function(node) { | ||
'CallExpression > MemberExpression.callee'(node) { | ||
if (isTopLevelScoped() || isInsideYieldOrAwait()) { | ||
@@ -43,12 +45,17 @@ return | ||
// if you're a then expression then you're probably a promise | ||
if (node.property && node.property.name === 'then') { | ||
// if you're a then/catch/finally expression then you're probably a promise | ||
if ( | ||
node.property && | ||
(node.property.name === 'then' || | ||
node.property.name === 'catch' || | ||
node.property.name === 'finally') | ||
) { | ||
context.report({ | ||
node: node.property, | ||
message: 'Prefer await to then().' | ||
message: 'Prefer await to then()/catch()/finally().', | ||
}) | ||
} | ||
} | ||
}, | ||
} | ||
} | ||
}, | ||
} |
@@ -8,7 +8,9 @@ 'use strict' | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: | ||
'Ensures the proper number of arguments are passed to Promise functions', | ||
url: getDocsUrl('valid-params') | ||
} | ||
url: getDocsUrl('valid-params'), | ||
}, | ||
schema: [], | ||
}, | ||
@@ -25,2 +27,3 @@ create(context) { | ||
// istanbul ignore next -- `isPromise` filters out others | ||
switch (name) { | ||
@@ -34,3 +37,3 @@ case 'resolve': | ||
'Promise.{{ name }}() requires 0 or 1 arguments, but received {{ numArgs }}', | ||
data: { name, numArgs } | ||
data: { name, numArgs }, | ||
}) | ||
@@ -45,3 +48,3 @@ } | ||
'Promise.{{ name }}() requires 1 or 2 arguments, but received {{ numArgs }}', | ||
data: { name, numArgs } | ||
data: { name, numArgs }, | ||
}) | ||
@@ -52,2 +55,4 @@ } | ||
case 'all': | ||
case 'allSettled': | ||
case 'any': | ||
case 'catch': | ||
@@ -60,3 +65,3 @@ case 'finally': | ||
'Promise.{{ name }}() requires 1 argument, but received {{ numArgs }}', | ||
data: { name, numArgs } | ||
data: { name, numArgs }, | ||
}) | ||
@@ -66,7 +71,8 @@ } | ||
default: | ||
// istanbul ignore next -- `isPromise` filters out others | ||
break | ||
} | ||
} | ||
}, | ||
} | ||
} | ||
}, | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
67120
13
29
1764
0
137
0
1