1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
|
/**
* @author Jamund Ferguson
* See LICENSE file in root directory for full license.
*/
"use strict"
module.exports = {
meta: {
docs: {
description: "require `return` statements after callbacks",
category: "Stylistic Issues",
recommended: false,
url:
"https://github.com/mysticatea/eslint-plugin-node/blob/v11.1.0/docs/rules/callback-return.md",
},
fixable: null,
messages: {
missingReturn: "Expected return with your callback function.",
},
schema: [
{
type: "array",
items: { type: "string" },
},
],
type: "suggestion",
},
create(context) {
const callbacks = context.options[0] || ["callback", "cb", "next"]
const sourceCode = context.getSourceCode()
/**
* Find the closest parent matching a list of types.
* @param {ASTNode} node The node whose parents we are searching
* @param {Array} types The node types to match
* @returns {ASTNode} The matched node or undefined.
*/
function findClosestParentOfType(node, types) {
if (!node.parent) {
return null
}
if (types.indexOf(node.parent.type) === -1) {
return findClosestParentOfType(node.parent, types)
}
return node.parent
}
/**
* Check to see if a node contains only identifers
* @param {ASTNode} node The node to check
* @returns {boolean} Whether or not the node contains only identifers
*/
function containsOnlyIdentifiers(node) {
if (node.type === "Identifier") {
return true
}
if (node.type === "MemberExpression") {
if (node.object.type === "Identifier") {
return true
}
if (node.object.type === "MemberExpression") {
return containsOnlyIdentifiers(node.object)
}
}
return false
}
/**
* Check to see if a CallExpression is in our callback list.
* @param {ASTNode} node The node to check against our callback names list.
* @returns {boolean} Whether or not this function matches our callback name.
*/
function isCallback(node) {
return (
containsOnlyIdentifiers(node.callee) &&
callbacks.indexOf(sourceCode.getText(node.callee)) > -1
)
}
/**
* Determines whether or not the callback is part of a callback expression.
* @param {ASTNode} node The callback node
* @param {ASTNode} parentNode The expression node
* @returns {boolean} Whether or not this is part of a callback expression
*/
function isCallbackExpression(node, parentNode) {
// ensure the parent node exists and is an expression
if (!parentNode || parentNode.type !== "ExpressionStatement") {
return false
}
// cb()
if (parentNode.expression === node) {
return true
}
// special case for cb && cb() and similar
if (
parentNode.expression.type === "BinaryExpression" ||
parentNode.expression.type === "LogicalExpression"
) {
if (parentNode.expression.right === node) {
return true
}
}
return false
}
return {
CallExpression(node) {
// if we're not a callback we can return
if (!isCallback(node)) {
return
}
// find the closest block, return or loop
const closestBlock =
findClosestParentOfType(node, [
"BlockStatement",
"ReturnStatement",
"ArrowFunctionExpression",
]) || {}
// if our parent is a return we know we're ok
if (closestBlock.type === "ReturnStatement") {
return
}
// arrow functions don't always have blocks and implicitly return
if (closestBlock.type === "ArrowFunctionExpression") {
return
}
// block statements are part of functions and most if statements
if (closestBlock.type === "BlockStatement") {
// find the last item in the block
const lastItem =
closestBlock.body[closestBlock.body.length - 1]
// if the callback is the last thing in a block that might be ok
if (isCallbackExpression(node, lastItem)) {
const parentType = closestBlock.parent.type
// but only if the block is part of a function
if (
parentType === "FunctionExpression" ||
parentType === "FunctionDeclaration" ||
parentType === "ArrowFunctionExpression"
) {
return
}
}
// ending a block with a return is also ok
if (lastItem.type === "ReturnStatement") {
// but only if the callback is immediately before
if (
isCallbackExpression(
node,
closestBlock.body[closestBlock.body.length - 2]
)
) {
return
}
}
}
// as long as you're the child of a function at this point you should be asked to return
if (
findClosestParentOfType(node, [
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
])
) {
context.report({ node, messageId: "missingReturn" })
}
},
}
},
}
|