/** * @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 }; } };