"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("@typescript-eslint/utils");
const ts = __importStar(require("typescript"));
const util_1 = require("../util");
const analyzeChain_1 = require("./prefer-optional-chain-utils/analyzeChain");
const checkNullishAndReport_1 = require("./prefer-optional-chain-utils/checkNullishAndReport");
const gatherLogicalOperands_1 = require("./prefer-optional-chain-utils/gatherLogicalOperands");
exports.default = (0, util_1.createRule)({
    name: 'prefer-optional-chain',
    meta: {
        type: 'suggestion',
        docs: {
            description: 'Enforce using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty objects',
            recommended: 'stylistic',
            requiresTypeChecking: true,
        },
        fixable: 'code',
        hasSuggestions: true,
        messages: {
            preferOptionalChain: "Prefer using an optional chain expression instead, as it's more concise and easier to read.",
            optionalChainSuggest: 'Change to an optional chain.',
        },
        schema: [
            {
                type: 'object',
                additionalProperties: false,
                properties: {
                    checkAny: {
                        type: 'boolean',
                        description: 'Check operands that are typed as `any` when inspecting "loose boolean" operands.',
                    },
                    checkUnknown: {
                        type: 'boolean',
                        description: 'Check operands that are typed as `unknown` when inspecting "loose boolean" operands.',
                    },
                    checkString: {
                        type: 'boolean',
                        description: 'Check operands that are typed as `string` when inspecting "loose boolean" operands.',
                    },
                    checkNumber: {
                        type: 'boolean',
                        description: 'Check operands that are typed as `number` when inspecting "loose boolean" operands.',
                    },
                    checkBoolean: {
                        type: 'boolean',
                        description: 'Check operands that are typed as `boolean` when inspecting "loose boolean" operands.',
                    },
                    checkBigInt: {
                        type: 'boolean',
                        description: 'Check operands that are typed as `bigint` when inspecting "loose boolean" operands.',
                    },
                    requireNullish: {
                        type: 'boolean',
                        description: 'Skip operands that are not typed with `null` and/or `undefined` when inspecting "loose boolean" operands.',
                    },
                    allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing: {
                        type: 'boolean',
                        description: 'Allow autofixers that will change the return type of the expression. This option is considered unsafe as it may break the build.',
                    },
                },
            },
        ],
    },
    defaultOptions: [
        {
            checkAny: true,
            checkUnknown: true,
            checkString: true,
            checkNumber: true,
            checkBoolean: true,
            checkBigInt: true,
            requireNullish: false,
            allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing: false,
        },
    ],
    create(context, [options]) {
        const parserServices = (0, util_1.getParserServices)(context);
        const seenLogicals = new Set();
        return {
            // specific handling for `(foo ?? {}).bar` / `(foo || {}).bar`
            'LogicalExpression[operator="||"], LogicalExpression[operator="??"]'(node) {
                const leftNode = node.left;
                const rightNode = node.right;
                const parentNode = node.parent;
                const isRightNodeAnEmptyObjectLiteral = rightNode.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
                    rightNode.properties.length === 0;
                if (!isRightNodeAnEmptyObjectLiteral ||
                    parentNode.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
                    parentNode.optional) {
                    return;
                }
                seenLogicals.add(node);
                function isLeftSideLowerPrecedence() {
                    const logicalTsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
                    const leftTsNode = parserServices.esTreeNodeToTSNodeMap.get(leftNode);
                    const operator = ts.isBinaryExpression(logicalTsNode)
                        ? logicalTsNode.operatorToken.kind
                        : ts.SyntaxKind.Unknown;
                    const leftPrecedence = (0, util_1.getOperatorPrecedence)(leftTsNode.kind, operator);
                    return leftPrecedence < util_1.OperatorPrecedence.LeftHandSide;
                }
                (0, checkNullishAndReport_1.checkNullishAndReport)(context, parserServices, options, [leftNode], {
                    messageId: 'preferOptionalChain',
                    node: parentNode,
                    suggest: [
                        {
                            messageId: 'optionalChainSuggest',
                            fix: (fixer) => {
                                const leftNodeText = context.sourceCode.getText(leftNode);
                                // Any node that is made of an operator with higher or equal precedence,
                                const maybeWrappedLeftNode = isLeftSideLowerPrecedence()
                                    ? `(${leftNodeText})`
                                    : leftNodeText;
                                const propertyToBeOptionalText = context.sourceCode.getText(parentNode.property);
                                const maybeWrappedProperty = parentNode.computed
                                    ? `[${propertyToBeOptionalText}]`
                                    : propertyToBeOptionalText;
                                return fixer.replaceTextRange(parentNode.range, `${maybeWrappedLeftNode}?.${maybeWrappedProperty}`);
                            },
                        },
                    ],
                });
            },
            'LogicalExpression[operator!="??"]'(node) {
                if (seenLogicals.has(node)) {
                    return;
                }
                const { operands, newlySeenLogicals } = (0, gatherLogicalOperands_1.gatherLogicalOperands)(node, parserServices, context.sourceCode, options);
                for (const logical of newlySeenLogicals) {
                    seenLogicals.add(logical);
                }
                let currentChain = [];
                for (const operand of operands) {
                    if (operand.type === "Invalid" /* OperandValidity.Invalid */) {
                        (0, analyzeChain_1.analyzeChain)(context, parserServices, options, node.operator, currentChain);
                        currentChain = [];
                    }
                    else {
                        currentChain.push(operand);
                    }
                }
                // make sure to check whatever's left
                if (currentChain.length > 0) {
                    (0, analyzeChain_1.analyzeChain)(context, parserServices, options, node.operator, currentChain);
                }
            },
        };
    },
});
//# sourceMappingURL=prefer-optional-chain.js.map