refactor start

This commit is contained in:
Spectre 2025-02-18 11:19:45 +01:00
parent bd49791e06
commit e46d25f0b7
16699 changed files with 2 additions and 1484887 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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