blob: 16f0070bc7c3bede3bd1120372e8020ce5111ea1 [file] [log] [blame]
/**
* @fileoverview Rule to flag consistent return values
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether or not a given node is an `Identifier` node which was named a given name.
* @param {ASTNode} node - A node to check.
* @param {string} name - An expected name of the node.
* @returns {boolean} `true` if the node is an `Identifier` node which was named as expected.
*/
function isIdentifier(node, name) {
return node.type === "Identifier" && node.name === name;
}
/**
* Checks whether or not a given code path segment is unreachable.
* @param {CodePathSegment} segment - A CodePathSegment to check.
* @returns {boolean} `true` if the segment is unreachable.
*/
function isUnreachable(segment) {
return !segment.reachable;
}
/**
* Checks whether a given node is a `constructor` method in an ES6 class
* @param {ASTNode} node A node to check
* @returns {boolean} `true` if the node is a `constructor` method
*/
function isClassConstructor(node) {
return node.type === "FunctionExpression" &&
node.parent &&
node.parent.type === "MethodDefinition" &&
node.parent.kind === "constructor";
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "require `return` statements to either always or never specify values",
category: "Best Practices",
recommended: false,
url: "https://eslint.org/docs/rules/consistent-return"
},
schema: [{
type: "object",
properties: {
treatUndefinedAsUnspecified: {
type: "boolean",
default: false
}
},
additionalProperties: false
}],
messages: {
missingReturn: "Expected to return a value at the end of {{name}}.",
missingReturnValue: "{{name}} expected a return value.",
unexpectedReturnValue: "{{name}} expected no return value."
}
},
create(context) {
const options = context.options[0] || {};
const treatUndefinedAsUnspecified = options.treatUndefinedAsUnspecified === true;
let funcInfo = null;
/**
* Checks whether of not the implicit returning is consistent if the last
* code path segment is reachable.
*
* @param {ASTNode} node - A program/function node to check.
* @returns {void}
*/
function checkLastSegment(node) {
let loc, name;
/*
* Skip if it expected no return value or unreachable.
* When unreachable, all paths are returned or thrown.
*/
if (!funcInfo.hasReturnValue ||
funcInfo.codePath.currentSegments.every(isUnreachable) ||
astUtils.isES5Constructor(node) ||
isClassConstructor(node)
) {
return;
}
// Adjust a location and a message.
if (node.type === "Program") {
// The head of program.
loc = { line: 1, column: 0 };
name = "program";
} else if (node.type === "ArrowFunctionExpression") {
// `=>` token
loc = context.getSourceCode().getTokenBefore(node.body, astUtils.isArrowToken).loc.start;
} else if (
node.parent.type === "MethodDefinition" ||
(node.parent.type === "Property" && node.parent.method)
) {
// Method name.
loc = node.parent.key.loc.start;
} else {
// Function name or `function` keyword.
loc = (node.id || node).loc.start;
}
if (!name) {
name = astUtils.getFunctionNameWithKind(node);
}
// Reports.
context.report({
node,
loc,
messageId: "missingReturn",
data: { name }
});
}
return {
// Initializes/Disposes state of each code path.
onCodePathStart(codePath, node) {
funcInfo = {
upper: funcInfo,
codePath,
hasReturn: false,
hasReturnValue: false,
messageId: "",
node
};
},
onCodePathEnd() {
funcInfo = funcInfo.upper;
},
// Reports a given return statement if it's inconsistent.
ReturnStatement(node) {
const argument = node.argument;
let hasReturnValue = Boolean(argument);
if (treatUndefinedAsUnspecified && hasReturnValue) {
hasReturnValue = !isIdentifier(argument, "undefined") && argument.operator !== "void";
}
if (!funcInfo.hasReturn) {
funcInfo.hasReturn = true;
funcInfo.hasReturnValue = hasReturnValue;
funcInfo.messageId = hasReturnValue ? "missingReturnValue" : "unexpectedReturnValue";
funcInfo.data = {
name: funcInfo.node.type === "Program"
? "Program"
: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
};
} else if (funcInfo.hasReturnValue !== hasReturnValue) {
context.report({
node,
messageId: funcInfo.messageId,
data: funcInfo.data
});
}
},
// Reports a given program/function if the implicit returning is not consistent.
"Program:exit": checkLastSegment,
"FunctionDeclaration:exit": checkLastSegment,
"FunctionExpression:exit": checkLastSegment,
"ArrowFunctionExpression:exit": checkLastSegment
};
}
};