/**
 * @fileoverview Checks for unreachable code due to return, throws, break, and continue.
 * @author Joel Feenstra
 */
"use strict";

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

/**
 * @typedef {Object} ConstructorInfo
 * @property {ConstructorInfo | null} upper Info about the constructor that encloses this constructor.
 * @property {boolean} hasSuperCall The flag about having `super()` expressions.
 */

/**
 * Checks whether or not a given variable declarator has the initializer.
 * @param {ASTNode} node A VariableDeclarator node to check.
 * @returns {boolean} `true` if the node has the initializer.
 */
function isInitialized(node) {
    return Boolean(node.init);
}

/**
 * Checks all segments in a set and returns true if all are unreachable.
 * @param {Set<CodePathSegment>} segments The segments to check.
 * @returns {boolean} True if all segments are unreachable; false otherwise.
 */
function areAllSegmentsUnreachable(segments) {

    for (const segment of segments) {
        if (segment.reachable) {
            return false;
        }
    }

    return true;
}

/**
 * The class to distinguish consecutive unreachable statements.
 */
class ConsecutiveRange {
    constructor(sourceCode) {
        this.sourceCode = sourceCode;
        this.startNode = null;
        this.endNode = null;
    }

    /**
     * The location object of this range.
     * @type {Object}
     */
    get location() {
        return {
            start: this.startNode.loc.start,
            end: this.endNode.loc.end
        };
    }

    /**
     * `true` if this range is empty.
     * @type {boolean}
     */
    get isEmpty() {
        return !(this.startNode && this.endNode);
    }

    /**
     * Checks whether the given node is inside of this range.
     * @param {ASTNode|Token} node The node to check.
     * @returns {boolean} `true` if the node is inside of this range.
     */
    contains(node) {
        return (
            node.range[0] >= this.startNode.range[0] &&
            node.range[1] <= this.endNode.range[1]
        );
    }

    /**
     * Checks whether the given node is consecutive to this range.
     * @param {ASTNode} node The node to check.
     * @returns {boolean} `true` if the node is consecutive to this range.
     */
    isConsecutive(node) {
        return this.contains(this.sourceCode.getTokenBefore(node));
    }

    /**
     * Merges the given node to this range.
     * @param {ASTNode} node The node to merge.
     * @returns {void}
     */
    merge(node) {
        this.endNode = node;
    }

    /**
     * Resets this range by the given node or null.
     * @param {ASTNode|null} node The node to reset, or null.
     * @returns {void}
     */
    reset(node) {
        this.startNode = this.endNode = node;
    }
}

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

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

        docs: {
            description: "Disallow unreachable code after `return`, `throw`, `continue`, and `break` statements",
            recommended: true,
            url: "https://eslint.org/docs/latest/rules/no-unreachable"
        },

        schema: [],

        messages: {
            unreachableCode: "Unreachable code."
        }
    },

    create(context) {

        /** @type {ConstructorInfo | null} */
        let constructorInfo = null;

        /** @type {ConsecutiveRange} */
        const range = new ConsecutiveRange(context.sourceCode);

        /** @type {Array<Set<CodePathSegment>>} */
        const codePathSegments = [];

        /** @type {Set<CodePathSegment>} */
        let currentCodePathSegments = new Set();

        /**
         * Reports a given node if it's unreachable.
         * @param {ASTNode} node A statement node to report.
         * @returns {void}
         */
        function reportIfUnreachable(node) {
            let nextNode = null;

            if (node && (node.type === "PropertyDefinition" || areAllSegmentsUnreachable(currentCodePathSegments))) {

                // Store this statement to distinguish consecutive statements.
                if (range.isEmpty) {
                    range.reset(node);
                    return;
                }

                // Skip if this statement is inside of the current range.
                if (range.contains(node)) {
                    return;
                }

                // Merge if this statement is consecutive to the current range.
                if (range.isConsecutive(node)) {
                    range.merge(node);
                    return;
                }

                nextNode = node;
            }

            /*
             * Report the current range since this statement is reachable or is
             * not consecutive to the current range.
             */
            if (!range.isEmpty) {
                context.report({
                    messageId: "unreachableCode",
                    loc: range.location,
                    node: range.startNode
                });
            }

            // Update the current range.
            range.reset(nextNode);
        }

        return {

            // Manages the current code path.
            onCodePathStart() {
                codePathSegments.push(currentCodePathSegments);
                currentCodePathSegments = new Set();
            },

            onCodePathEnd() {
                currentCodePathSegments = codePathSegments.pop();
            },

            onUnreachableCodePathSegmentStart(segment) {
                currentCodePathSegments.add(segment);
            },

            onUnreachableCodePathSegmentEnd(segment) {
                currentCodePathSegments.delete(segment);
            },

            onCodePathSegmentEnd(segment) {
                currentCodePathSegments.delete(segment);
            },

            onCodePathSegmentStart(segment) {
                currentCodePathSegments.add(segment);
            },

            // Registers for all statement nodes (excludes FunctionDeclaration).
            BlockStatement: reportIfUnreachable,
            BreakStatement: reportIfUnreachable,
            ClassDeclaration: reportIfUnreachable,
            ContinueStatement: reportIfUnreachable,
            DebuggerStatement: reportIfUnreachable,
            DoWhileStatement: reportIfUnreachable,
            ExpressionStatement: reportIfUnreachable,
            ForInStatement: reportIfUnreachable,
            ForOfStatement: reportIfUnreachable,
            ForStatement: reportIfUnreachable,
            IfStatement: reportIfUnreachable,
            ImportDeclaration: reportIfUnreachable,
            LabeledStatement: reportIfUnreachable,
            ReturnStatement: reportIfUnreachable,
            SwitchStatement: reportIfUnreachable,
            ThrowStatement: reportIfUnreachable,
            TryStatement: reportIfUnreachable,

            VariableDeclaration(node) {
                if (node.kind !== "var" || node.declarations.some(isInitialized)) {
                    reportIfUnreachable(node);
                }
            },

            WhileStatement: reportIfUnreachable,
            WithStatement: reportIfUnreachable,
            ExportNamedDeclaration: reportIfUnreachable,
            ExportDefaultDeclaration: reportIfUnreachable,
            ExportAllDeclaration: reportIfUnreachable,

            "Program:exit"() {
                reportIfUnreachable();
            },

            /*
             * Instance fields defined in a subclass are never created if the constructor of the subclass
             * doesn't call `super()`, so their definitions are unreachable code.
             */
            "MethodDefinition[kind='constructor']"() {
                constructorInfo = {
                    upper: constructorInfo,
                    hasSuperCall: false
                };
            },
            "MethodDefinition[kind='constructor']:exit"(node) {
                const { hasSuperCall } = constructorInfo;

                constructorInfo = constructorInfo.upper;

                // skip typescript constructors without the body
                if (!node.value.body) {
                    return;
                }

                const classDefinition = node.parent.parent;

                if (classDefinition.superClass && !hasSuperCall) {
                    for (const element of classDefinition.body.body) {
                        if (element.type === "PropertyDefinition" && !element.static) {
                            reportIfUnreachable(element);
                        }
                    }
                }
            },
            "CallExpression > Super.callee"() {
                if (constructorInfo) {
                    constructorInfo.hasSuperCall = true;
                }
            }
        };
    }
};
