/**
 * @fileoverview Enforces that a return statement is present in property getters.
 * @author Aladdin-ADD(hh_2013@foxmail.com)
 */

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const astUtils = require("./utils/ast-utils");

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;

/**
 * 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: {
        type: "problem",

        docs: {
            description: "enforce `return` statements in getters",
            category: "Possible Errors",
            recommended: true,
            url: "https://eslint.org/docs/rules/getter-return"
        },

        fixable: null,

        schema: [
            {
                type: "object",
                properties: {
                    allowImplicit: {
                        type: "boolean",
                        default: false
                    }
                },
                additionalProperties: false
            }
        ],

        messages: {
            expected: "Expected to return a value in {{name}}.",
            expectedAlways: "Expected {{name}} to always return a value."
        }
    },

    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,
                    messageId: funcInfo.hasReturn ? "expectedAlways" : "expected",
                    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,
                            messageId: "expected",
                            data: {
                                name: astUtils.getFunctionNameWithKind(funcInfo.node)
                            }
                        });
                    }
                }
            },

            // Reports a given function if the last path is reachable.
            "FunctionExpression:exit": checkLastSegment,
            "ArrowFunctionExpression:exit": checkLastSegment
        };
    }
};
