/** * @fileoverview Enforces that a return statement is present in property getters. * @author Aladdin-ADD(hh_2013@foxmail.com) */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const astUtils = require("../ast-utils"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/; /** * Checks a given code path segment is reachable. * * @param {CodePathSegment} segment - A segment to check. * @returns {boolean} `true` if the segment is reachable. */ function isReachable(segment) { return segment.reachable; } /** * Gets a readable location. * * - FunctionExpression -> the function name or `function` keyword. * * @param {ASTNode} node - A function node to get. * @returns {ASTNode|Token} The node or the token of a location. */ function getId(node) { return node.id || node; } //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: "enforce `return` statements in getters", category: "Possible Errors", recommended: false }, fixable: null, schema: [ { type: "object", properties: { allowImplicit: { type: "boolean" } }, additionalProperties: false } ] }, create(context) { const options = context.options[0] || { allowImplicit: false }; let funcInfo = { upper: null, codePath: null, hasReturn: false, shouldCheck: false, node: null }; /** * Checks whether or not the last code path segment is reachable. * Then reports this function if the segment is reachable. * * If the last code path segment is reachable, there are paths which are not * returned or thrown. * * @param {ASTNode} node - A node to check. * @returns {void} */ function checkLastSegment(node) { if (funcInfo.shouldCheck && funcInfo.codePath.currentSegments.some(isReachable) ) { context.report({ node, loc: getId(node).loc.start, message: funcInfo.hasReturn ? "Expected {{name}} to always return a value." : "Expected to return a value in {{name}}.", data: { name: astUtils.getFunctionNameWithKind(funcInfo.node) } }); } } /** * Checks whether a node means a getter function. * @param {ASTNode} node - a node to check. * @returns {boolean} if node means a getter, return true; else return false. */ function isGetter(node) { const parent = node.parent; if (TARGET_NODE_TYPE.test(node.type) && node.body.type === "BlockStatement") { if (parent.kind === "get") { return true; } if (parent.type === "Property" && astUtils.getStaticPropertyName(parent) === "get" && parent.parent.type === "ObjectExpression") { // Object.defineProperty() if (parent.parent.parent.type === "CallExpression" && astUtils.getStaticPropertyName(parent.parent.parent.callee) === "defineProperty") { return true; } // Object.defineProperties() if (parent.parent.parent.type === "Property" && parent.parent.parent.parent.type === "ObjectExpression" && parent.parent.parent.parent.parent.type === "CallExpression" && astUtils.getStaticPropertyName(parent.parent.parent.parent.parent.callee) === "defineProperties") { return true; } } } return false; } return { // Stacks this function's information. onCodePathStart(codePath, node) { funcInfo = { upper: funcInfo, codePath, hasReturn: false, shouldCheck: isGetter(node), node }; }, // Pops this function's information. onCodePathEnd() { funcInfo = funcInfo.upper; }, // Checks the return statement is valid. ReturnStatement(node) { if (funcInfo.shouldCheck) { funcInfo.hasReturn = true; // if allowImplicit: false, should also check node.argument if (!options.allowImplicit && !node.argument) { context.report({ node, message: "Expected to return a value in {{name}}.", data: { name: astUtils.getFunctionNameWithKind(funcInfo.node) } }); } } }, // Reports a given function if the last path is reachable. "FunctionExpression:exit": checkLastSegment, "ArrowFunctionExpression:exit": checkLastSegment }; } };