"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("@typescript-eslint/utils");
const eslint_utils_1 = require("@typescript-eslint/utils/eslint-utils");
const util_1 = require("../util");
const LT = `[${Array.from(new Set(['\r\n', '\r', '\n', '\u2028', '\u2029'])).join('')}]`;
const PADDING_LINE_SEQUENCE = new RegExp(String.raw `^(\s*?${LT})\s*${LT}(\s*;?)$`, 'u');
/**
 * Creates tester which check if a node starts with specific keyword with the
 * appropriate AST_NODE_TYPES.
 * @param keyword The keyword to test.
 * @returns the created tester.
 * @private
 */
function newKeywordTester(type, keyword) {
    return {
        test(node, sourceCode) {
            const isSameKeyword = sourceCode.getFirstToken(node)?.value === keyword;
            const isSameType = Array.isArray(type)
                ? type.some(val => val === node.type)
                : type === node.type;
            return isSameKeyword && isSameType;
        },
    };
}
/**
 * Creates tester which check if a node starts with specific keyword and spans a single line.
 * @param keyword The keyword to test.
 * @returns the created tester.
 * @private
 */
function newSinglelineKeywordTester(keyword) {
    return {
        test(node, sourceCode) {
            return (node.loc.start.line === node.loc.end.line &&
                sourceCode.getFirstToken(node).value === keyword);
        },
    };
}
/**
 * Creates tester which check if a node starts with specific keyword and spans multiple lines.
 * @param keyword The keyword to test.
 * @returns the created tester.
 * @private
 */
function newMultilineKeywordTester(keyword) {
    return {
        test(node, sourceCode) {
            return (node.loc.start.line !== node.loc.end.line &&
                sourceCode.getFirstToken(node).value === keyword);
        },
    };
}
/**
 * Creates tester which check if a node is specific type.
 * @param type The node type to test.
 * @returns the created tester.
 * @private
 */
function newNodeTypeTester(type) {
    return {
        test: (node) => node.type === type,
    };
}
/**
 * Skips a chain expression node
 * @param node The node to test
 * @returnsA non-chain expression
 * @private
 */
function skipChainExpression(node) {
    return node.type === utils_1.AST_NODE_TYPES.ChainExpression ? node.expression : node;
}
/**
 * Checks the given node is an expression statement of IIFE.
 * @param node The node to check.
 * @returns `true` if the node is an expression statement of IIFE.
 * @private
 */
function isIIFEStatement(node) {
    if (node.type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
        let expression = skipChainExpression(node.expression);
        if (expression.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
            expression = skipChainExpression(expression.argument);
        }
        if (expression.type === utils_1.AST_NODE_TYPES.CallExpression) {
            let node = expression.callee;
            while (node.type === utils_1.AST_NODE_TYPES.SequenceExpression) {
                node = node.expressions[node.expressions.length - 1];
            }
            return (0, util_1.isFunction)(node);
        }
    }
    return false;
}
/**
 * Checks the given node is a CommonJS require statement
 * @param node The node to check.
 * @returns `true` if the node is a CommonJS require statement.
 * @private
 */
function isCJSRequire(node) {
    if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
        const declaration = node.declarations.at(0);
        if (declaration?.init) {
            let call = declaration.init;
            while (call.type === utils_1.AST_NODE_TYPES.MemberExpression) {
                call = call.object;
            }
            if (call.type === utils_1.AST_NODE_TYPES.CallExpression &&
                call.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
                return call.callee.name === 'require';
            }
        }
    }
    return false;
}
/**
 * Checks whether the given node is a block-like statement.
 * This checks the last token of the node is the closing brace of a block.
 * @param sourceCode The source code to get tokens.
 * @param node The node to check.
 * @returns `true` if the node is a block-like statement.
 * @private
 */
function isBlockLikeStatement(node, sourceCode) {
    // do-while with a block is a block-like statement.
    if (node.type === utils_1.AST_NODE_TYPES.DoWhileStatement &&
        node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
        return true;
    }
    /**
     * IIFE is a block-like statement specially from
     * JSCS#disallowPaddingNewLinesAfterBlocks.
     */
    if (isIIFEStatement(node)) {
        return true;
    }
    // Checks the last token is a closing brace of blocks.
    const lastToken = sourceCode.getLastToken(node, util_1.isNotSemicolonToken);
    const belongingNode = lastToken && (0, util_1.isClosingBraceToken)(lastToken)
        ? sourceCode.getNodeByRangeIndex(lastToken.range[0])
        : null;
    return (!!belongingNode &&
        (belongingNode.type === utils_1.AST_NODE_TYPES.BlockStatement ||
            belongingNode.type === utils_1.AST_NODE_TYPES.SwitchStatement));
}
/**
 * Check whether the given node is a directive or not.
 * @param node The node to check.
 * @param sourceCode The source code object to get tokens.
 * @returns `true` if the node is a directive.
 */
function isDirective(node, sourceCode) {
    return (node.type === utils_1.AST_NODE_TYPES.ExpressionStatement &&
        (node.parent.type === utils_1.AST_NODE_TYPES.Program ||
            (node.parent.type === utils_1.AST_NODE_TYPES.BlockStatement &&
                (0, util_1.isFunction)(node.parent.parent))) &&
        node.expression.type === utils_1.AST_NODE_TYPES.Literal &&
        typeof node.expression.value === 'string' &&
        !(0, util_1.isParenthesized)(node.expression, sourceCode));
}
/**
 * Check whether the given node is a part of directive prologue or not.
 * @param node The node to check.
 * @param sourceCode The source code object to get tokens.
 * @returns `true` if the node is a part of directive prologue.
 */
function isDirectivePrologue(node, sourceCode) {
    if (isDirective(node, sourceCode) &&
        node.parent &&
        'body' in node.parent &&
        Array.isArray(node.parent.body)) {
        for (const sibling of node.parent.body) {
            if (sibling === node) {
                break;
            }
            if (!isDirective(sibling, sourceCode)) {
                return false;
            }
        }
        return true;
    }
    return false;
}
/**
 * Checks the given node is a CommonJS export statement
 * @param node The node to check.
 * @returns `true` if the node is a CommonJS export statement.
 * @private
 */
function isCJSExport(node) {
    if (node.type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
        const expression = node.expression;
        if (expression.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
            let left = expression.left;
            if (left.type === utils_1.AST_NODE_TYPES.MemberExpression) {
                while (left.object.type === utils_1.AST_NODE_TYPES.MemberExpression) {
                    left = left.object;
                }
                return (left.object.type === utils_1.AST_NODE_TYPES.Identifier &&
                    (left.object.name === 'exports' ||
                        (left.object.name === 'module' &&
                            left.property.type === utils_1.AST_NODE_TYPES.Identifier &&
                            left.property.name === 'exports')));
            }
        }
    }
    return false;
}
/**
 * Check whether the given node is an expression
 * @param node The node to check.
 * @param sourceCode The source code object to get tokens.
 * @returns `true` if the node is an expression
 */
function isExpression(node, sourceCode) {
    return (node.type === utils_1.AST_NODE_TYPES.ExpressionStatement &&
        !isDirectivePrologue(node, sourceCode));
}
/**
 * Gets the actual last token.
 *
 * If a semicolon is semicolon-less style's semicolon, this ignores it.
 * For example:
 *
 *     foo()
 *     ;[1, 2, 3].forEach(bar)
 * @param sourceCode The source code to get tokens.
 * @param node The node to get.
 * @returns The actual last token.
 * @private
 */
function getActualLastToken(node, sourceCode) {
    const semiToken = sourceCode.getLastToken(node);
    const prevToken = sourceCode.getTokenBefore(semiToken);
    const nextToken = sourceCode.getTokenAfter(semiToken);
    const isSemicolonLessStyle = prevToken &&
        nextToken &&
        prevToken.range[0] >= node.range[0] &&
        (0, util_1.isSemicolonToken)(semiToken) &&
        semiToken.loc.start.line !== prevToken.loc.end.line &&
        semiToken.loc.end.line === nextToken.loc.start.line;
    return isSemicolonLessStyle ? prevToken : semiToken;
}
/**
 * This returns the concatenation of the first 2 captured strings.
 * @param _ Unused. Whole matched string.
 * @param trailingSpaces The trailing spaces of the first line.
 * @param indentSpaces The indentation spaces of the last line.
 * @returns The concatenation of trailingSpaces and indentSpaces.
 * @private
 */
function replacerToRemovePaddingLines(_, trailingSpaces, indentSpaces) {
    return trailingSpaces + indentSpaces;
}
/**
 * Check and report statements for `any` configuration.
 * It does nothing.
 *
 * @private
 */
function verifyForAny() {
    // Empty
}
/**
 * Check and report statements for `never` configuration.
 * This autofix removes blank lines between the given 2 statements.
 * However, if comments exist between 2 blank lines, it does not remove those
 * blank lines automatically.
 * @param context The rule context to report.
 * @param _ Unused. The previous node to check.
 * @param nextNode The next node to check.
 * @param paddingLines The array of token pairs that blank
 * lines exist between the pair.
 *
 * @private
 */
function verifyForNever(context, _, nextNode, paddingLines) {
    if (paddingLines.length === 0) {
        return;
    }
    context.report({
        node: nextNode,
        messageId: 'unexpectedBlankLine',
        fix(fixer) {
            if (paddingLines.length >= 2) {
                return null;
            }
            const prevToken = paddingLines[0][0];
            const nextToken = paddingLines[0][1];
            const start = prevToken.range[1];
            const end = nextToken.range[0];
            const text = (0, eslint_utils_1.getSourceCode)(context)
                .text.slice(start, end)
                .replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines);
            return fixer.replaceTextRange([start, end], text);
        },
    });
}
/**
 * Check and report statements for `always` configuration.
 * This autofix inserts a blank line between the given 2 statements.
 * If the `prevNode` has trailing comments, it inserts a blank line after the
 * trailing comments.
 * @param context The rule context to report.
 * @param prevNode The previous node to check.
 * @param nextNode The next node to check.
 * @param paddingLines The array of token pairs that blank
 * lines exist between the pair.
 *
 * @private
 */
function verifyForAlways(context, prevNode, nextNode, paddingLines) {
    if (paddingLines.length > 0) {
        return;
    }
    context.report({
        node: nextNode,
        messageId: 'expectedBlankLine',
        fix(fixer) {
            const sourceCode = (0, eslint_utils_1.getSourceCode)(context);
            let prevToken = getActualLastToken(prevNode, sourceCode);
            const nextToken = sourceCode.getFirstTokenBetween(prevToken, nextNode, {
                includeComments: true,
                /**
                 * Skip the trailing comments of the previous node.
                 * This inserts a blank line after the last trailing comment.
                 *
                 * For example:
                 *
                 *     foo(); // trailing comment.
                 *     // comment.
                 *     bar();
                 *
                 * Get fixed to:
                 *
                 *     foo(); // trailing comment.
                 *
                 *     // comment.
                 *     bar();
                 * @param token The token to check.
                 * @returns `true` if the token is not a trailing comment.
                 * @private
                 */
                filter(token) {
                    if ((0, util_1.isTokenOnSameLine)(prevToken, token)) {
                        prevToken = token;
                        return false;
                    }
                    return true;
                },
            }) || nextNode;
            const insertText = (0, util_1.isTokenOnSameLine)(prevToken, nextToken)
                ? '\n\n'
                : '\n';
            return fixer.insertTextAfter(prevToken, insertText);
        },
    });
}
/**
 * Types of blank lines.
 * `any`, `never`, and `always` are defined.
 * Those have `verify` method to check and report statements.
 * @private
 */
const PaddingTypes = {
    any: { verify: verifyForAny },
    never: { verify: verifyForNever },
    always: { verify: verifyForAlways },
};
/**
 * Types of statements.
 * Those have `test` method to check it matches to the given statement.
 * @private
 */
const StatementTypes = {
    '*': { test: () => true },
    'block-like': { test: isBlockLikeStatement },
    exports: { test: isCJSExport },
    require: { test: isCJSRequire },
    directive: { test: isDirectivePrologue },
    expression: { test: isExpression },
    iife: { test: isIIFEStatement },
    'multiline-block-like': {
        test: (node, sourceCode) => node.loc.start.line !== node.loc.end.line &&
            isBlockLikeStatement(node, sourceCode),
    },
    'multiline-expression': {
        test: (node, sourceCode) => node.loc.start.line !== node.loc.end.line &&
            node.type === utils_1.AST_NODE_TYPES.ExpressionStatement &&
            !isDirectivePrologue(node, sourceCode),
    },
    'multiline-const': newMultilineKeywordTester('const'),
    'multiline-let': newMultilineKeywordTester('let'),
    'multiline-var': newMultilineKeywordTester('var'),
    'singleline-const': newSinglelineKeywordTester('const'),
    'singleline-let': newSinglelineKeywordTester('let'),
    'singleline-var': newSinglelineKeywordTester('var'),
    block: newNodeTypeTester(utils_1.AST_NODE_TYPES.BlockStatement),
    empty: newNodeTypeTester(utils_1.AST_NODE_TYPES.EmptyStatement),
    function: newNodeTypeTester(utils_1.AST_NODE_TYPES.FunctionDeclaration),
    break: newKeywordTester(utils_1.AST_NODE_TYPES.BreakStatement, 'break'),
    case: newKeywordTester(utils_1.AST_NODE_TYPES.SwitchCase, 'case'),
    class: newKeywordTester(utils_1.AST_NODE_TYPES.ClassDeclaration, 'class'),
    const: newKeywordTester(utils_1.AST_NODE_TYPES.VariableDeclaration, 'const'),
    continue: newKeywordTester(utils_1.AST_NODE_TYPES.ContinueStatement, 'continue'),
    debugger: newKeywordTester(utils_1.AST_NODE_TYPES.DebuggerStatement, 'debugger'),
    default: newKeywordTester([utils_1.AST_NODE_TYPES.SwitchCase, utils_1.AST_NODE_TYPES.ExportDefaultDeclaration], 'default'),
    do: newKeywordTester(utils_1.AST_NODE_TYPES.DoWhileStatement, 'do'),
    export: newKeywordTester([
        utils_1.AST_NODE_TYPES.ExportDefaultDeclaration,
        utils_1.AST_NODE_TYPES.ExportNamedDeclaration,
    ], 'export'),
    for: newKeywordTester([
        utils_1.AST_NODE_TYPES.ForStatement,
        utils_1.AST_NODE_TYPES.ForInStatement,
        utils_1.AST_NODE_TYPES.ForOfStatement,
    ], 'for'),
    if: newKeywordTester(utils_1.AST_NODE_TYPES.IfStatement, 'if'),
    import: newKeywordTester(utils_1.AST_NODE_TYPES.ImportDeclaration, 'import'),
    let: newKeywordTester(utils_1.AST_NODE_TYPES.VariableDeclaration, 'let'),
    return: newKeywordTester(utils_1.AST_NODE_TYPES.ReturnStatement, 'return'),
    switch: newKeywordTester(utils_1.AST_NODE_TYPES.SwitchStatement, 'switch'),
    throw: newKeywordTester(utils_1.AST_NODE_TYPES.ThrowStatement, 'throw'),
    try: newKeywordTester(utils_1.AST_NODE_TYPES.TryStatement, 'try'),
    var: newKeywordTester(utils_1.AST_NODE_TYPES.VariableDeclaration, 'var'),
    while: newKeywordTester([utils_1.AST_NODE_TYPES.WhileStatement, utils_1.AST_NODE_TYPES.DoWhileStatement], 'while'),
    with: newKeywordTester(utils_1.AST_NODE_TYPES.WithStatement, 'with'),
    // Additional Typescript constructs
    interface: newKeywordTester(utils_1.AST_NODE_TYPES.TSInterfaceDeclaration, 'interface'),
    type: newKeywordTester(utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration, 'type'),
};
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
exports.default = (0, util_1.createRule)({
    name: 'padding-line-between-statements',
    meta: {
        type: 'layout',
        docs: {
            description: 'Require or disallow padding lines between statements',
            extendsBaseRule: true,
        },
        fixable: 'whitespace',
        hasSuggestions: false,
        // This is intentionally an array schema as you can pass 0..n config objects
        schema: {
            $defs: {
                paddingType: {
                    type: 'string',
                    enum: Object.keys(PaddingTypes),
                },
                statementType: {
                    anyOf: [
                        {
                            type: 'string',
                            enum: Object.keys(StatementTypes),
                        },
                        {
                            type: 'array',
                            items: {
                                type: 'string',
                                enum: Object.keys(StatementTypes),
                            },
                            minItems: 1,
                            uniqueItems: true,
                            additionalItems: false,
                        },
                    ],
                },
            },
            type: 'array',
            additionalItems: false,
            items: {
                type: 'object',
                properties: {
                    blankLine: { $ref: '#/$defs/paddingType' },
                    prev: { $ref: '#/$defs/statementType' },
                    next: { $ref: '#/$defs/statementType' },
                },
                additionalProperties: false,
                required: ['blankLine', 'prev', 'next'],
            },
        },
        messages: {
            unexpectedBlankLine: 'Unexpected blank line before this statement.',
            expectedBlankLine: 'Expected blank line before this statement.',
        },
    },
    defaultOptions: [],
    create(context) {
        const sourceCode = (0, eslint_utils_1.getSourceCode)(context);
        // eslint-disable-next-line no-restricted-syntax -- We need all raw options.
        const configureList = context.options;
        let scopeInfo = null;
        /**
         * Processes to enter to new scope.
         * This manages the current previous statement.
         *
         * @private
         */
        function enterScope() {
            scopeInfo = {
                upper: scopeInfo,
                prevNode: null,
            };
        }
        /**
         * Processes to exit from the current scope.
         *
         * @private
         */
        function exitScope() {
            if (scopeInfo) {
                scopeInfo = scopeInfo.upper;
            }
        }
        /**
         * Checks whether the given node matches the given type.
         * @param node The statement node to check.
         * @param type The statement type to check.
         * @returns `true` if the statement node matched the type.
         * @private
         */
        function match(node, type) {
            let innerStatementNode = node;
            while (innerStatementNode.type === utils_1.AST_NODE_TYPES.LabeledStatement) {
                innerStatementNode = innerStatementNode.body;
            }
            if (Array.isArray(type)) {
                return type.some(match.bind(null, innerStatementNode));
            }
            return StatementTypes[type].test(innerStatementNode, sourceCode);
        }
        /**
         * Finds the last matched configure from configureList.
         * @paramprevNode The previous statement to match.
         * @paramnextNode The current statement to match.
         * @returns The tester of the last matched configure.
         * @private
         */
        function getPaddingType(prevNode, nextNode) {
            for (let i = configureList.length - 1; i >= 0; --i) {
                const configure = configureList[i];
                if (match(prevNode, configure.prev) &&
                    match(nextNode, configure.next)) {
                    return PaddingTypes[configure.blankLine];
                }
            }
            return PaddingTypes.any;
        }
        /**
         * Gets padding line sequences between the given 2 statements.
         * Comments are separators of the padding line sequences.
         * @paramprevNode The previous statement to count.
         * @paramnextNode The current statement to count.
         * @returns The array of token pairs.
         * @private
         */
        function getPaddingLineSequences(prevNode, nextNode) {
            const pairs = [];
            let prevToken = getActualLastToken(prevNode, sourceCode);
            if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) {
                do {
                    const token = sourceCode.getTokenAfter(prevToken, {
                        includeComments: true,
                    });
                    if (token.loc.start.line - prevToken.loc.end.line >= 2) {
                        pairs.push([prevToken, token]);
                    }
                    prevToken = token;
                } while (prevToken.range[0] < nextNode.range[0]);
            }
            return pairs;
        }
        /**
         * Verify padding lines between the given node and the previous node.
         * @param node The node to verify.
         *
         * @private
         */
        function verify(node) {
            if (!node.parent ||
                ![
                    utils_1.AST_NODE_TYPES.BlockStatement,
                    utils_1.AST_NODE_TYPES.Program,
                    utils_1.AST_NODE_TYPES.SwitchCase,
                    utils_1.AST_NODE_TYPES.SwitchStatement,
                    utils_1.AST_NODE_TYPES.TSModuleBlock,
                ].includes(node.parent.type)) {
                return;
            }
            // Save this node as the current previous statement.
            const prevNode = scopeInfo.prevNode;
            // Verify.
            if (prevNode) {
                const type = getPaddingType(prevNode, node);
                const paddingLines = getPaddingLineSequences(prevNode, node);
                type.verify(context, prevNode, node, paddingLines);
            }
            scopeInfo.prevNode = node;
        }
        /**
         * Verify padding lines between the given node and the previous node.
         * Then process to enter to new scope.
         * @param node The node to verify.
         *
         * @private
         */
        function verifyThenEnterScope(node) {
            verify(node);
            enterScope();
        }
        return {
            Program: enterScope,
            BlockStatement: enterScope,
            SwitchStatement: enterScope,
            TSModuleBlock: enterScope,
            'Program:exit': exitScope,
            'BlockStatement:exit': exitScope,
            'SwitchStatement:exit': exitScope,
            'TSModuleBlock:exit': exitScope,
            ':statement': verify,
            SwitchCase: verifyThenEnterScope,
            TSDeclareFunction: verifyThenEnterScope,
            'SwitchCase:exit': exitScope,
            'TSDeclareFunction:exit': exitScope,
        };
    },
});
//# sourceMappingURL=padding-line-between-statements.js.map