"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 tsutils = __importStar(require("ts-api-utils"));
const ts = __importStar(require("typescript"));
const util_1 = require("../util");
const literalToPrimitiveTypeFlags = {
    [ts.TypeFlags.BigIntLiteral]: ts.TypeFlags.BigInt,
    [ts.TypeFlags.BooleanLiteral]: ts.TypeFlags.Boolean,
    [ts.TypeFlags.NumberLiteral]: ts.TypeFlags.Number,
    [ts.TypeFlags.StringLiteral]: ts.TypeFlags.String,
    [ts.TypeFlags.TemplateLiteral]: ts.TypeFlags.String,
};
const literalTypeFlags = [
    ts.TypeFlags.BigIntLiteral,
    ts.TypeFlags.BooleanLiteral,
    ts.TypeFlags.NumberLiteral,
    ts.TypeFlags.StringLiteral,
    ts.TypeFlags.TemplateLiteral,
];
const primitiveTypeFlags = [
    ts.TypeFlags.BigInt,
    ts.TypeFlags.Boolean,
    ts.TypeFlags.Number,
    ts.TypeFlags.String,
];
const primitiveTypeFlagNames = {
    [ts.TypeFlags.BigInt]: 'bigint',
    [ts.TypeFlags.Boolean]: 'boolean',
    [ts.TypeFlags.Number]: 'number',
    [ts.TypeFlags.String]: 'string',
};
const primitiveTypeFlagTypes = {
    bigint: ts.TypeFlags.BigIntLiteral,
    boolean: ts.TypeFlags.BooleanLiteral,
    number: ts.TypeFlags.NumberLiteral,
    string: ts.TypeFlags.StringLiteral,
};
const keywordNodeTypesToTsTypes = new Map([
    [utils_1.TSESTree.AST_NODE_TYPES.TSAnyKeyword, ts.TypeFlags.Any],
    [utils_1.TSESTree.AST_NODE_TYPES.TSBigIntKeyword, ts.TypeFlags.BigInt],
    [utils_1.TSESTree.AST_NODE_TYPES.TSBooleanKeyword, ts.TypeFlags.Boolean],
    [utils_1.TSESTree.AST_NODE_TYPES.TSNeverKeyword, ts.TypeFlags.Never],
    [utils_1.TSESTree.AST_NODE_TYPES.TSUnknownKeyword, ts.TypeFlags.Unknown],
    [utils_1.TSESTree.AST_NODE_TYPES.TSNumberKeyword, ts.TypeFlags.Number],
    [utils_1.TSESTree.AST_NODE_TYPES.TSStringKeyword, ts.TypeFlags.String],
]);
function addToMapGroup(map, key, value) {
    const existing = map.get(key);
    if (existing) {
        existing.push(value);
    }
    else {
        map.set(key, [value]);
    }
}
function describeLiteralType(type) {
    if (type.isStringLiteral()) {
        return JSON.stringify(type.value);
    }
    if ((0, util_1.isTypeBigIntLiteralType)(type)) {
        return `${type.value.negative ? '-' : ''}${type.value.base10Value}n`;
    }
    if (type.isLiteral()) {
        // eslint-disable-next-line @typescript-eslint/no-base-to-string
        return type.value.toString();
    }
    if ((0, util_1.isTypeAnyType)(type)) {
        return 'any';
    }
    if ((0, util_1.isTypeNeverType)(type)) {
        return 'never';
    }
    if ((0, util_1.isTypeUnknownType)(type)) {
        return 'unknown';
    }
    if ((0, util_1.isTypeTemplateLiteralType)(type)) {
        return 'template literal type';
    }
    if ((0, util_1.isTypeBigIntLiteralType)(type)) {
        return `${type.value.negative ? '-' : ''}${type.value.base10Value}n`;
    }
    if (tsutils.isTrueLiteralType(type)) {
        return 'true';
    }
    if (tsutils.isFalseLiteralType(type)) {
        return 'false';
    }
    return 'literal type';
}
function describeLiteralTypeNode(typeNode) {
    switch (typeNode.type) {
        case utils_1.AST_NODE_TYPES.TSAnyKeyword:
            return 'any';
        case utils_1.AST_NODE_TYPES.TSBooleanKeyword:
            return 'boolean';
        case utils_1.AST_NODE_TYPES.TSNeverKeyword:
            return 'never';
        case utils_1.AST_NODE_TYPES.TSNumberKeyword:
            return 'number';
        case utils_1.AST_NODE_TYPES.TSStringKeyword:
            return 'string';
        case utils_1.AST_NODE_TYPES.TSUnknownKeyword:
            return 'unknown';
        case utils_1.AST_NODE_TYPES.TSLiteralType:
            switch (typeNode.literal.type) {
                case utils_1.TSESTree.AST_NODE_TYPES.Literal:
                    switch (typeof typeNode.literal.value) {
                        case 'bigint':
                            return `${typeNode.literal.value < 0 ? '-' : ''}${typeNode.literal.value}n`;
                        case 'string':
                            return JSON.stringify(typeNode.literal.value);
                        default:
                            return `${typeNode.literal.value}`;
                    }
                case utils_1.TSESTree.AST_NODE_TYPES.TemplateLiteral:
                    return 'template literal type';
            }
    }
    return 'literal type';
}
function isNodeInsideReturnType(node) {
    return !!(node.parent.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation &&
        ((0, util_1.isFunctionType)(node.parent.parent) || (0, util_1.isFunction)(node.parent.parent)));
}
/**
 * @remarks TypeScript stores boolean types as the union false | true, always.
 */
function unionTypePartsUnlessBoolean(type) {
    return type.isUnion() &&
        type.types.length === 2 &&
        tsutils.isFalseLiteralType(type.types[0]) &&
        tsutils.isTrueLiteralType(type.types[1])
        ? [type]
        : tsutils.unionTypeParts(type);
}
exports.default = (0, util_1.createRule)({
    name: 'no-redundant-type-constituents',
    meta: {
        docs: {
            description: 'Disallow members of unions and intersections that do nothing or override type information',
            recommended: 'recommended',
            requiresTypeChecking: true,
        },
        messages: {
            literalOverridden: `{{literal}} is overridden by {{primitive}} in this union type.`,
            primitiveOverridden: `{{primitive}} is overridden by the {{literal}} in this intersection type.`,
            overridden: `'{{typeName}}' is overridden by other types in this {{container}} type.`,
            overrides: `'{{typeName}}' overrides all other types in this {{container}} type.`,
        },
        schema: [],
        type: 'suggestion',
    },
    defaultOptions: [],
    create(context) {
        const services = (0, util_1.getParserServices)(context);
        const typesCache = new Map();
        function getTypeNodeTypePartFlags(typeNode) {
            const keywordTypeFlags = keywordNodeTypesToTsTypes.get(typeNode.type);
            if (keywordTypeFlags) {
                return [
                    {
                        typeFlags: keywordTypeFlags,
                        typeName: describeLiteralTypeNode(typeNode),
                    },
                ];
            }
            if (typeNode.type === utils_1.AST_NODE_TYPES.TSLiteralType &&
                typeNode.literal.type === utils_1.AST_NODE_TYPES.Literal) {
                return [
                    {
                        typeFlags: primitiveTypeFlagTypes[typeof typeNode.literal
                            .value],
                        typeName: describeLiteralTypeNode(typeNode),
                    },
                ];
            }
            if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType) {
                return typeNode.types.flatMap(getTypeNodeTypePartFlags);
            }
            const nodeType = services.getTypeAtLocation(typeNode);
            const typeParts = unionTypePartsUnlessBoolean(nodeType);
            return typeParts.map(typePart => ({
                typeFlags: typePart.flags,
                typeName: describeLiteralType(typePart),
            }));
        }
        function getTypeNodeTypePartFlagsCached(typeNode) {
            const existing = typesCache.get(typeNode);
            if (existing) {
                return existing;
            }
            const created = getTypeNodeTypePartFlags(typeNode);
            typesCache.set(typeNode, created);
            return created;
        }
        return {
            'TSIntersectionType:exit'(node) {
                const seenLiteralTypes = new Map();
                const seenPrimitiveTypes = new Map();
                const seenUnionTypes = new Map();
                function checkIntersectionBottomAndTopTypes({ typeFlags, typeName }, typeNode) {
                    for (const [messageId, checkFlag] of [
                        ['overrides', ts.TypeFlags.Any],
                        ['overrides', ts.TypeFlags.Never],
                        ['overridden', ts.TypeFlags.Unknown],
                    ]) {
                        if (typeFlags === checkFlag) {
                            context.report({
                                data: {
                                    container: 'intersection',
                                    typeName,
                                },
                                messageId,
                                node: typeNode,
                            });
                            return true;
                        }
                    }
                    return false;
                }
                for (const typeNode of node.types) {
                    const typePartFlags = getTypeNodeTypePartFlagsCached(typeNode);
                    for (const typePart of typePartFlags) {
                        if (checkIntersectionBottomAndTopTypes(typePart, typeNode)) {
                            continue;
                        }
                        for (const literalTypeFlag of literalTypeFlags) {
                            if (typePart.typeFlags === literalTypeFlag) {
                                addToMapGroup(seenLiteralTypes, literalToPrimitiveTypeFlags[literalTypeFlag], typePart.typeName);
                                break;
                            }
                        }
                        for (const primitiveTypeFlag of primitiveTypeFlags) {
                            if (typePart.typeFlags === primitiveTypeFlag) {
                                addToMapGroup(seenPrimitiveTypes, primitiveTypeFlag, typeNode);
                            }
                        }
                    }
                    // if any typeNode is TSTypeReference and typePartFlags have more than 1 element, than the referenced type is definitely a union.
                    if (typePartFlags.length >= 2) {
                        seenUnionTypes.set(typeNode, typePartFlags);
                    }
                }
                /**
                 * @example
                 * ```ts
                 * type F = "a"|2|"b";
                 * type I = F & string;
                 * ```
                 * This function checks if all the union members of `F` are assignable to the other member of `I`. If every member is assignable, then its reported else not.
                 */
                const checkIfUnionsAreAssignable = () => {
                    for (const [typeRef, typeValues] of seenUnionTypes) {
                        let primitive = undefined;
                        for (const { typeFlags } of typeValues) {
                            if (seenPrimitiveTypes.has(literalToPrimitiveTypeFlags[typeFlags])) {
                                primitive =
                                    literalToPrimitiveTypeFlags[typeFlags];
                            }
                            else {
                                primitive = undefined;
                                break;
                            }
                        }
                        if (Number.isInteger(primitive)) {
                            context.report({
                                data: {
                                    literal: typeValues.map(name => name.typeName).join(' | '),
                                    primitive: primitiveTypeFlagNames[primitive],
                                },
                                messageId: 'primitiveOverridden',
                                node: typeRef,
                            });
                        }
                    }
                };
                if (seenUnionTypes.size > 0) {
                    checkIfUnionsAreAssignable();
                    return;
                }
                // For each primitive type of all the seen primitive types,
                // if there was a literal type seen that overrides it,
                // report each of the primitive type's type nodes
                for (const [primitiveTypeFlag, typeNodes] of seenPrimitiveTypes) {
                    const matchedLiteralTypes = seenLiteralTypes.get(primitiveTypeFlag);
                    if (matchedLiteralTypes) {
                        for (const typeNode of typeNodes) {
                            context.report({
                                data: {
                                    literal: matchedLiteralTypes.join(' | '),
                                    primitive: primitiveTypeFlagNames[primitiveTypeFlag],
                                },
                                messageId: 'primitiveOverridden',
                                node: typeNode,
                            });
                        }
                    }
                }
            },
            'TSUnionType:exit'(node) {
                const seenLiteralTypes = new Map();
                const seenPrimitiveTypes = new Set();
                function checkUnionBottomAndTopTypes({ typeFlags, typeName }, typeNode) {
                    for (const checkFlag of [
                        ts.TypeFlags.Any,
                        ts.TypeFlags.Unknown,
                    ]) {
                        if (typeFlags === checkFlag) {
                            context.report({
                                data: {
                                    container: 'union',
                                    typeName,
                                },
                                messageId: 'overrides',
                                node: typeNode,
                            });
                            return true;
                        }
                    }
                    if (typeFlags === ts.TypeFlags.Never &&
                        !isNodeInsideReturnType(node)) {
                        context.report({
                            data: {
                                container: 'union',
                                typeName: 'never',
                            },
                            messageId: 'overridden',
                            node: typeNode,
                        });
                        return true;
                    }
                    return false;
                }
                for (const typeNode of node.types) {
                    const typePartFlags = getTypeNodeTypePartFlagsCached(typeNode);
                    for (const typePart of typePartFlags) {
                        if (checkUnionBottomAndTopTypes(typePart, typeNode)) {
                            continue;
                        }
                        for (const literalTypeFlag of literalTypeFlags) {
                            if (typePart.typeFlags === literalTypeFlag) {
                                addToMapGroup(seenLiteralTypes, literalToPrimitiveTypeFlags[literalTypeFlag], {
                                    literalValue: typePart.typeName,
                                    typeNode,
                                });
                                break;
                            }
                        }
                        for (const primitiveTypeFlag of primitiveTypeFlags) {
                            if ((typePart.typeFlags & primitiveTypeFlag) !== 0) {
                                seenPrimitiveTypes.add(primitiveTypeFlag);
                            }
                        }
                    }
                }
                const overriddenTypeNodes = new Map();
                // For each primitive type of all the seen literal types,
                // if there was a primitive type seen that overrides it,
                // upsert the literal text and primitive type under the backing type node
                for (const [primitiveTypeFlag, typeNodesWithText] of seenLiteralTypes) {
                    if (seenPrimitiveTypes.has(primitiveTypeFlag)) {
                        for (const { literalValue, typeNode } of typeNodesWithText) {
                            addToMapGroup(overriddenTypeNodes, typeNode, {
                                literalValue,
                                primitiveTypeFlag,
                            });
                        }
                    }
                }
                // For each type node that had at least one overridden literal,
                // group those literals by their primitive type,
                // then report each primitive type with all its literals
                for (const [typeNode, typeFlagsWithText] of overriddenTypeNodes) {
                    const grouped = (0, util_1.arrayGroupByToMap)(typeFlagsWithText, pair => pair.primitiveTypeFlag);
                    for (const [primitiveTypeFlag, pairs] of grouped) {
                        context.report({
                            data: {
                                literal: pairs.map(pair => pair.literalValue).join(' | '),
                                primitive: primitiveTypeFlagNames[primitiveTypeFlag],
                            },
                            messageId: 'literalOverridden',
                            node: typeNode,
                        });
                    }
                }
            },
        };
    },
});
//# sourceMappingURL=no-redundant-type-constituents.js.map