"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 });
exports.analyzeChain = void 0;
const utils_1 = require("@typescript-eslint/utils");
const ts_api_utils_1 = require("ts-api-utils");
const ts = __importStar(require("typescript"));
const util = __importStar(require("../../util"));
const compareNodes_1 = require("./compareNodes");
function includesType(parserServices, node, typeFlagIn) {
    const typeFlag = typeFlagIn | ts.TypeFlags.Any | ts.TypeFlags.Unknown;
    const types = (0, ts_api_utils_1.unionTypeParts)(parserServices.getTypeAtLocation(node));
    for (const type of types) {
        if (util.isTypeFlagSet(type, typeFlag)) {
            return true;
        }
    }
    return false;
}
const analyzeAndChainOperand = (parserServices, operand, index, chain) => {
    switch (operand.comparisonType) {
        case "Boolean" /* NullishComparisonType.Boolean */:
        case "NotEqualNullOrUndefined" /* NullishComparisonType.NotEqualNullOrUndefined */:
            return [operand];
        case "NotStrictEqualNull" /* NullishComparisonType.NotStrictEqualNull */: {
            // handle `x !== null && x !== undefined`
            const nextOperand = chain[index + 1];
            if (nextOperand?.comparisonType ===
                "NotStrictEqualUndefined" /* NullishComparisonType.NotStrictEqualUndefined */ &&
                (0, compareNodes_1.compareNodes)(operand.comparedName, nextOperand.comparedName) ===
                    "Equal" /* NodeComparisonResult.Equal */) {
                return [operand, nextOperand];
            }
            if (includesType(parserServices, operand.comparedName, ts.TypeFlags.Undefined)) {
                // we know the next operand is not an `undefined` check and that this
                // operand includes `undefined` - which means that making this an
                // optional chain would change the runtime behavior of the expression
                return null;
            }
            return [operand];
        }
        case "NotStrictEqualUndefined" /* NullishComparisonType.NotStrictEqualUndefined */: {
            // handle `x !== undefined && x !== null`
            const nextOperand = chain[index + 1];
            if (nextOperand?.comparisonType ===
                "NotStrictEqualNull" /* NullishComparisonType.NotStrictEqualNull */ &&
                (0, compareNodes_1.compareNodes)(operand.comparedName, nextOperand.comparedName) ===
                    "Equal" /* NodeComparisonResult.Equal */) {
                return [operand, nextOperand];
            }
            if (includesType(parserServices, operand.comparedName, ts.TypeFlags.Null)) {
                // we know the next operand is not a `null` check and that this
                // operand includes `null` - which means that making this an
                // optional chain would change the runtime behavior of the expression
                return null;
            }
            return [operand];
        }
        default:
            return null;
    }
};
const analyzeOrChainOperand = (parserServices, operand, index, chain) => {
    switch (operand.comparisonType) {
        case "NotBoolean" /* NullishComparisonType.NotBoolean */:
        case "EqualNullOrUndefined" /* NullishComparisonType.EqualNullOrUndefined */:
            return [operand];
        case "StrictEqualNull" /* NullishComparisonType.StrictEqualNull */: {
            // handle `x === null || x === undefined`
            const nextOperand = chain[index + 1];
            if (nextOperand?.comparisonType ===
                "StrictEqualUndefined" /* NullishComparisonType.StrictEqualUndefined */ &&
                (0, compareNodes_1.compareNodes)(operand.comparedName, nextOperand.comparedName) ===
                    "Equal" /* NodeComparisonResult.Equal */) {
                return [operand, nextOperand];
            }
            if (includesType(parserServices, operand.comparedName, ts.TypeFlags.Undefined)) {
                // we know the next operand is not an `undefined` check and that this
                // operand includes `undefined` - which means that making this an
                // optional chain would change the runtime behavior of the expression
                return null;
            }
            return [operand];
        }
        case "StrictEqualUndefined" /* NullishComparisonType.StrictEqualUndefined */: {
            // handle `x === undefined || x === null`
            const nextOperand = chain[index + 1];
            if (nextOperand?.comparisonType === "StrictEqualNull" /* NullishComparisonType.StrictEqualNull */ &&
                (0, compareNodes_1.compareNodes)(operand.comparedName, nextOperand.comparedName) ===
                    "Equal" /* NodeComparisonResult.Equal */) {
                return [operand, nextOperand];
            }
            if (includesType(parserServices, operand.comparedName, ts.TypeFlags.Null)) {
                // we know the next operand is not a `null` check and that this
                // operand includes `null` - which means that making this an
                // optional chain would change the runtime behavior of the expression
                return null;
            }
            return [operand];
        }
        default:
            return null;
    }
};
function getFixer(sourceCode, parserServices, operator, options, chain) {
    const lastOperand = chain[chain.length - 1];
    let useSuggestionFixer;
    if (options.allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing ===
        true) {
        // user has opted-in to the unsafe behavior
        useSuggestionFixer = false;
    }
    else {
        // optional chain specifically will union `undefined` into the final type
        // so we need to make sure that there is at least one operand that includes
        // `undefined`, or else we're going to change the final type - which is
        // unsafe and might cause downstream type errors.
        if (lastOperand.comparisonType ===
            "EqualNullOrUndefined" /* NullishComparisonType.EqualNullOrUndefined */ ||
            lastOperand.comparisonType ===
                "NotEqualNullOrUndefined" /* NullishComparisonType.NotEqualNullOrUndefined */ ||
            lastOperand.comparisonType ===
                "StrictEqualUndefined" /* NullishComparisonType.StrictEqualUndefined */ ||
            lastOperand.comparisonType ===
                "NotStrictEqualUndefined" /* NullishComparisonType.NotStrictEqualUndefined */ ||
            (operator === '||' &&
                lastOperand.comparisonType === "NotBoolean" /* NullishComparisonType.NotBoolean */)) {
            // we know the last operand is an equality check - so the change in types
            // DOES NOT matter and will not change the runtime result or cause a type
            // check error
            useSuggestionFixer = false;
        }
        else {
            useSuggestionFixer = true;
            for (const operand of chain) {
                if (includesType(parserServices, operand.node, ts.TypeFlags.Undefined)) {
                    useSuggestionFixer = false;
                    break;
                }
            }
            // TODO - we could further reduce the false-positive rate of this check by
            //        checking for cases where the change in types don't matter like
            //        the test location of an if/while/etc statement.
            //        but it's quite complex to do this without false-negatives, so
            //        for now we'll just be over-eager with our matching.
            //
            //        it's MUCH better to false-positive here and only provide a
            //        suggestion fixer, rather than false-negative and autofix to
            //        broken code.
        }
    }
    // In its most naive form we could just slap `?.` for every single part of the
    // chain. However this would be undesirable because it'd create unnecessary
    // conditions in the user's code where there were none before - and it would
    // cause errors with rules like our `no-unnecessary-condition`.
    //
    // Instead we want to include the minimum number of `?.` required to correctly
    // unify the code into a single chain. Naively you might think that we can
    // just take the final operand add `?.` after the locations from the previous
    // operands - however this won't be correct either because earlier operands
    // can include a necessary `?.` that's not needed or included in a later
    // operand.
    //
    // So instead what we need to do is to start at the first operand and
    // iteratively diff it against the next operand, and add the difference to the
    // first operand.
    //
    // eg
    // `foo && foo.bar && foo.bar.baz?.bam && foo.bar.baz.bam()`
    // 1) `foo`
    // 2) diff(`foo`, `foo.bar`) = `.bar`
    // 3) result = `foo?.bar`
    // 4) diff(`foo.bar`, `foo.bar.baz?.bam`) = `.baz?.bam`
    // 5) result = `foo?.bar?.baz?.bam`
    // 6) diff(`foo.bar.baz?.bam`, `foo.bar.baz.bam()`) = `()`
    // 7) result = `foo?.bar?.baz?.bam?.()`
    const parts = [];
    for (const current of chain) {
        const nextOperand = flattenChainExpression(sourceCode, current.comparedName);
        const diff = nextOperand.slice(parts.length);
        if (diff.length > 0) {
            if (parts.length > 0) {
                // we need to make the first operand of the diff optional so it matches the
                // logic before merging
                // foo.bar && foo.bar.baz
                // diff = .baz
                // result = foo.bar?.baz
                diff[0].optional = true;
            }
            parts.push(...diff);
        }
    }
    let newCode = parts
        .map(part => {
        let str = '';
        if (part.optional) {
            str += '?.';
        }
        else {
            if (part.nonNull) {
                str += '!';
            }
            if (part.requiresDot) {
                str += '.';
            }
        }
        if (part.precedence !== util.OperatorPrecedence.Invalid &&
            part.precedence < util.OperatorPrecedence.Member) {
            str += `(${part.text})`;
        }
        else {
            str += part.text;
        }
        return str;
    })
        .join('');
    if (lastOperand.node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
        // retain the ending comparison for cases like
        // x && x.a != null
        // x && typeof x.a !== 'undefined'
        const operator = lastOperand.node.operator;
        const { left, right } = (() => {
            if (lastOperand.isYoda) {
                const unaryOperator = lastOperand.node.right.type === utils_1.AST_NODE_TYPES.UnaryExpression
                    ? lastOperand.node.right.operator + ' '
                    : '';
                return {
                    left: sourceCode.getText(lastOperand.node.left),
                    right: unaryOperator + newCode,
                };
            }
            const unaryOperator = lastOperand.node.left.type === utils_1.AST_NODE_TYPES.UnaryExpression
                ? lastOperand.node.left.operator + ' '
                : '';
            return {
                left: unaryOperator + newCode,
                right: sourceCode.getText(lastOperand.node.right),
            };
        })();
        newCode = `${left} ${operator} ${right}`;
    }
    else if (lastOperand.comparisonType === "NotBoolean" /* NullishComparisonType.NotBoolean */) {
        newCode = `!${newCode}`;
    }
    const fix = fixer => fixer.replaceTextRange([chain[0].node.range[0], lastOperand.node.range[1]], newCode);
    return useSuggestionFixer
        ? { suggest: [{ fix, messageId: 'optionalChainSuggest' }] }
        : { fix };
    function flattenChainExpression(sourceCode, node) {
        switch (node.type) {
            case utils_1.AST_NODE_TYPES.ChainExpression:
                return flattenChainExpression(sourceCode, node.expression);
            case utils_1.AST_NODE_TYPES.CallExpression: {
                const argumentsText = (() => {
                    const closingParenToken = util.nullThrows(sourceCode.getLastToken(node), util.NullThrowsReasons.MissingToken('closing parenthesis', node.type));
                    const openingParenToken = util.nullThrows(sourceCode.getFirstTokenBetween(node.typeArguments ?? node.callee, closingParenToken, util.isOpeningParenToken), util.NullThrowsReasons.MissingToken('opening parenthesis', node.type));
                    return sourceCode.text.substring(openingParenToken.range[0], closingParenToken.range[1]);
                })();
                const typeArgumentsText = (() => {
                    if (node.typeArguments == null) {
                        return '';
                    }
                    return sourceCode.getText(node.typeArguments);
                })();
                return [
                    ...flattenChainExpression(sourceCode, node.callee),
                    {
                        nonNull: false,
                        optional: node.optional,
                        // no precedence for this
                        precedence: util.OperatorPrecedence.Invalid,
                        requiresDot: false,
                        text: typeArgumentsText + argumentsText,
                    },
                ];
            }
            case utils_1.AST_NODE_TYPES.MemberExpression: {
                const propertyText = sourceCode.getText(node.property);
                return [
                    ...flattenChainExpression(sourceCode, node.object),
                    {
                        nonNull: node.object.type === utils_1.AST_NODE_TYPES.TSNonNullExpression,
                        optional: node.optional,
                        precedence: node.computed
                            ? // computed is already wrapped in [] so no need to wrap in () as well
                                util.OperatorPrecedence.Invalid
                            : util.getOperatorPrecedenceForNode(node.property),
                        requiresDot: !node.computed,
                        text: node.computed ? `[${propertyText}]` : propertyText,
                    },
                ];
            }
            case utils_1.AST_NODE_TYPES.TSNonNullExpression:
                return flattenChainExpression(sourceCode, node.expression);
            default:
                return [
                    {
                        nonNull: false,
                        optional: false,
                        precedence: util.getOperatorPrecedenceForNode(node),
                        requiresDot: false,
                        text: sourceCode.getText(node),
                    },
                ];
        }
    }
}
function analyzeChain(context, sourceCode, parserServices, options, operator, chain) {
    // need at least 2 operands in a chain for it to be a chain
    if (chain.length <= 1 ||
        /* istanbul ignore next -- previous checks make this unreachable, but keep it for exhaustiveness check */
        operator === '??') {
        return;
    }
    const analyzeOperand = (() => {
        switch (operator) {
            case '&&':
                return analyzeAndChainOperand;
            case '||':
                return analyzeOrChainOperand;
        }
    })();
    let subChain = [];
    const maybeReportThenReset = (newChainSeed) => {
        if (subChain.length > 1) {
            context.report({
                messageId: 'preferOptionalChain',
                loc: {
                    start: subChain[0].node.loc.start,
                    end: subChain[subChain.length - 1].node.loc.end,
                },
                ...getFixer(sourceCode, parserServices, operator, options, subChain),
            });
        }
        // we've reached the end of a chain of logical expressions
        // i.e. the current operand doesn't belong to the previous chain.
        //
        // we don't want to throw away the current operand otherwise we will skip it
        // and that can cause us to miss chains. So instead we seed the new chain
        // with the current operand
        //
        // eg this means we can catch cases like:
        //     unrelated != null && foo != null && foo.bar != null;
        //     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ first "chain"
        //                          ^^^^^^^^^^^ newChainSeed
        //                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ second chain
        subChain = newChainSeed ? [...newChainSeed] : [];
    };
    for (let i = 0; i < chain.length; i += 1) {
        const lastOperand = subChain[subChain.length - 1];
        const operand = chain[i];
        const validatedOperands = analyzeOperand(parserServices, operand, i, chain);
        if (!validatedOperands) {
            // TODO - #7170
            // check if the name is a superset/equal - if it is, then it likely
            // intended to be part of the chain and something we should include in the
            // report, eg
            //     foo == null || foo.bar;
            //     ^^^^^^^^^^^ valid OR chain
            //                    ^^^^^^^ invalid OR chain logical, but still part of
            //                            the chain for combination purposes
            maybeReportThenReset();
            continue;
        }
        // in case multiple operands were consumed - make sure to correctly increment the index
        i += validatedOperands.length - 1;
        const currentOperand = validatedOperands[0];
        if (lastOperand) {
            const comparisonResult = (0, compareNodes_1.compareNodes)(lastOperand.comparedName, 
            // purposely inspect and push the last operand because the prior operands don't matter
            // this also means we won't false-positive in cases like
            // foo !== null && foo !== undefined
            validatedOperands[validatedOperands.length - 1].comparedName);
            if (comparisonResult === "Subset" /* NodeComparisonResult.Subset */) {
                // the operands are comparable, so we can continue searching
                subChain.push(currentOperand);
            }
            else if (comparisonResult === "Invalid" /* NodeComparisonResult.Invalid */) {
                maybeReportThenReset(validatedOperands);
            }
            else if (comparisonResult === "Equal" /* NodeComparisonResult.Equal */) {
                // purposely don't push this case because the node is a no-op and if
                // we consider it then we might report on things like
                // foo && foo
            }
        }
        else {
            subChain.push(currentOperand);
        }
    }
    // check the leftovers
    maybeReportThenReset();
}
exports.analyzeChain = analyzeChain;
//# sourceMappingURL=analyzeChain.js.map