/**
 * @fileoverview Rule to flag the use of redundant constructors in classes.
 * @author Alberto Rodríguez
 */
"use strict";

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

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

/**
 * Checks whether a given array of statements is a single call of `super`.
 * @param {ASTNode[]} body An array of statements to check.
 * @returns {boolean} `true` if the body is a single call of `super`.
 */
function isSingleSuperCall(body) {
    return (
        body.length === 1 &&
        body[0].type === "ExpressionStatement" &&
        body[0].expression.type === "CallExpression" &&
        body[0].expression.callee.type === "Super"
    );
}

/**
 * Checks whether a given node is a pattern which doesn't have any side effects.
 * Default parameters and Destructuring parameters can have side effects.
 * @param {ASTNode} node A pattern node.
 * @returns {boolean} `true` if the node doesn't have any side effects.
 */
function isSimple(node) {
    return node.type === "Identifier" || node.type === "RestElement";
}

/**
 * Checks whether a given array of expressions is `...arguments` or not.
 * `super(...arguments)` passes all arguments through.
 * @param {ASTNode[]} superArgs An array of expressions to check.
 * @returns {boolean} `true` if the superArgs is `...arguments`.
 */
function isSpreadArguments(superArgs) {
    return (
        superArgs.length === 1 &&
        superArgs[0].type === "SpreadElement" &&
        superArgs[0].argument.type === "Identifier" &&
        superArgs[0].argument.name === "arguments"
    );
}

/**
 * Checks whether given 2 nodes are identifiers which have the same name or not.
 * @param {ASTNode} ctorParam A node to check.
 * @param {ASTNode} superArg A node to check.
 * @returns {boolean} `true` if the nodes are identifiers which have the same
 *      name.
 */
function isValidIdentifierPair(ctorParam, superArg) {
    return (
        ctorParam.type === "Identifier" &&
        superArg.type === "Identifier" &&
        ctorParam.name === superArg.name
    );
}

/**
 * Checks whether given 2 nodes are a rest/spread pair which has the same values.
 * @param {ASTNode} ctorParam A node to check.
 * @param {ASTNode} superArg A node to check.
 * @returns {boolean} `true` if the nodes are a rest/spread pair which has the
 *      same values.
 */
function isValidRestSpreadPair(ctorParam, superArg) {
    return (
        ctorParam.type === "RestElement" &&
        superArg.type === "SpreadElement" &&
        isValidIdentifierPair(ctorParam.argument, superArg.argument)
    );
}

/**
 * Checks whether given 2 nodes have the same value or not.
 * @param {ASTNode} ctorParam A node to check.
 * @param {ASTNode} superArg A node to check.
 * @returns {boolean} `true` if the nodes have the same value or not.
 */
function isValidPair(ctorParam, superArg) {
    return (
        isValidIdentifierPair(ctorParam, superArg) ||
        isValidRestSpreadPair(ctorParam, superArg)
    );
}

/**
 * Checks whether the parameters of a constructor and the arguments of `super()`
 * have the same values or not.
 * @param {ASTNode} ctorParams The parameters of a constructor to check.
 * @param {ASTNode} superArgs The arguments of `super()` to check.
 * @returns {boolean} `true` if those have the same values.
 */
function isPassingThrough(ctorParams, superArgs) {
    if (ctorParams.length !== superArgs.length) {
        return false;
    }

    for (let i = 0; i < ctorParams.length; ++i) {
        if (!isValidPair(ctorParams[i], superArgs[i])) {
            return false;
        }
    }

    return true;
}

/**
 * Checks whether the constructor body is a redundant super call.
 * @param {Array} body constructor body content.
 * @param {Array} ctorParams The params to check against super call.
 * @returns {boolean} true if the constructor body is redundant
 */
function isRedundantSuperCall(body, ctorParams) {
    return (
        isSingleSuperCall(body) &&
        ctorParams.every(isSimple) &&
        (
            isSpreadArguments(body[0].expression.arguments) ||
            isPassingThrough(ctorParams, body[0].expression.arguments)
        )
    );
}

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('../shared/types').Rule} */
module.exports = {
    meta: {
        type: "suggestion",

        docs: {
            description: "Disallow unnecessary constructors",
            recommended: false,
            url: "https://eslint.org/docs/latest/rules/no-useless-constructor"
        },

        hasSuggestions: true,

        schema: [],

        messages: {
            noUselessConstructor: "Useless constructor.",
            removeConstructor: "Remove the constructor."
        }
    },

    create(context) {

        /**
         * Checks whether a node is a redundant constructor
         * @param {ASTNode} node node to check
         * @returns {void}
         */
        function checkForConstructor(node) {
            if (node.kind !== "constructor") {
                return;
            }

            /*
             * Prevent crashing on parsers which do not require class constructor
             * to have a body, e.g. typescript and flow
             */
            if (!node.value.body) {
                return;
            }

            const body = node.value.body.body;
            const ctorParams = node.value.params;
            const superClass = node.parent.parent.superClass;

            if (superClass ? isRedundantSuperCall(body, ctorParams) : (body.length === 0)) {
                context.report({
                    node,
                    messageId: "noUselessConstructor",
                    suggest: [
                        {
                            messageId: "removeConstructor",
                            *fix(fixer) {
                                const nextToken = context.sourceCode.getTokenAfter(node);
                                const addSemiColon = nextToken.type === "Punctuator" && nextToken.value === "[" && astUtils.needsPrecedingSemicolon(context.sourceCode, node);

                                yield fixer.replaceText(node, addSemiColon ? ";" : "");
                            }
                        }
                    ]
                });
            }
        }

        return {
            MethodDefinition: checkForConstructor
        };
    }
};
