"""
Format expression type checker.

This file is conceptually part of ExpressionChecker and TypeChecker. Main functionality
is located in StringFormatterChecker.check_str_format_call() for '{}'.format(), and in
StringFormatterChecker.check_str_interpolation() for printf-style % interpolation.

Note that although at runtime format strings are parsed using custom parsers,
here we use a regexp-based approach. This way we 99% match runtime behaviour while keeping
implementation simple.
"""

from __future__ import annotations

import re
from typing import TYPE_CHECKING, Callable, Dict, Final, Match, Pattern, Tuple, Union, cast
from typing_extensions import TypeAlias as _TypeAlias

import mypy.errorcodes as codes
from mypy.errors import Errors
from mypy.nodes import (
    ARG_NAMED,
    ARG_POS,
    ARG_STAR,
    ARG_STAR2,
    BytesExpr,
    CallExpr,
    Context,
    DictExpr,
    Expression,
    ExpressionStmt,
    IndexExpr,
    IntExpr,
    MemberExpr,
    MypyFile,
    NameExpr,
    Node,
    StarExpr,
    StrExpr,
    TempNode,
    TupleExpr,
)
from mypy.types import (
    AnyType,
    Instance,
    LiteralType,
    TupleType,
    Type,
    TypeOfAny,
    TypeVarTupleType,
    TypeVarType,
    UnionType,
    UnpackType,
    find_unpack_in_list,
    get_proper_type,
    get_proper_types,
)

if TYPE_CHECKING:
    # break import cycle only needed for mypy
    import mypy.checker
    import mypy.checkexpr

from mypy import message_registry
from mypy.maptype import map_instance_to_supertype
from mypy.messages import MessageBuilder
from mypy.parse import parse
from mypy.subtypes import is_subtype
from mypy.typeops import custom_special_method

FormatStringExpr: _TypeAlias = Union[StrExpr, BytesExpr]
Checkers: _TypeAlias = Tuple[Callable[[Expression], None], Callable[[Type], bool]]
MatchMap: _TypeAlias = Dict[Tuple[int, int], Match[str]]  # span -> match

def compile_format_re() -> Pattern[str]:
    """Construct regexp to match format conversion specifiers in % interpolation.

    See https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting
    The regexp is intentionally a bit wider to report better errors.
    """
    key_re = r"(\((?P<key>[^)]*)\))?"  # (optional) parenthesised sequence of characters.
    flags_re = r"(?P<flags>[#0\-+ ]*)"  # (optional) sequence of flags.
    width_re = r"(?P<width>[1-9][0-9]*|\*)?"  # (optional) minimum field width (* or numbers).
    precision_re = r"(?:\.(?P<precision>\*|[0-9]+)?)?"  # (optional) . followed by * of numbers.
    length_mod_re = r"[hlL]?"  # (optional) length modifier (unused).
    type_re = r"(?P<type>.)?"  # conversion type.
    format_re = "%" + key_re + flags_re + width_re + precision_re + length_mod_re + type_re
    return re.compile(format_re)

def compile_new_format_re(custom_spec: bool) -> Pattern[str]:
    """Construct regexps to match format conversion specifiers in str.format() calls.

    See After https://docs.python.org/3/library/string.html#formatspec for
    specifications. The regexps are intentionally wider, to report better errors,
    instead of just not matching.
    """

    # Field (optional) is an integer/identifier possibly followed by several .attr and [index].
    field = r"(?P<field>(?P<key>[^.[!:]*)([^:!]+)?)"

    # Conversion (optional) is ! followed by one of letters for forced repr(), str(), or ascii().
    conversion = r"(?P<conversion>![^:])?"

    # Format specification (optional) follows its own mini-language:
    if not custom_spec:
        # Fill and align is valid for all builtin types.
        fill_align = r"(?P<fill_align>.?[<>=^])?"
        # Number formatting options are only valid for int, float, complex, and Decimal,
        # except if only width is given (it is valid for all types).
        # This contains sign, flags (sign, # and/or 0), width, grouping (_ or ,) and precision.
        num_spec = r"(?P<flags>[+\- ]?#?0?)(?P<width>\d+)?[_,]?(?P<precision>\.\d+)?"
        # The last element is type.
        conv_type = r"(?P<type>.)?"  # only some are supported, but we want to give a better error
        format_spec = r"(?P<format_spec>:" + fill_align + num_spec + conv_type + r")?"
    else:
        # Custom types can define their own form_spec using __format__().
        format_spec = r"(?P<format_spec>:.*)?"

    return re.compile(field + conversion + format_spec)

FORMAT_RE: Final = compile_format_re()
FORMAT_RE_NEW: Final = compile_new_format_re(False)
FORMAT_RE_NEW_CUSTOM: Final = compile_new_format_re(True)
DUMMY_FIELD_NAME: Final = "__dummy_name__"

# Types that require either int or float.
NUMERIC_TYPES_OLD: Final = {"d", "i", "o", "u", "x", "X", "e", "E", "f", "F", "g", "G"}
NUMERIC_TYPES_NEW: Final = {"b", "d", "o", "e", "E", "f", "F", "g", "G", "n", "x", "X", "%"}

# These types accept _only_ int.
REQUIRE_INT_OLD: Final = {"o", "x", "X"}
REQUIRE_INT_NEW: Final = {"b", "d", "o", "x", "X"}

# These types fall back to SupportsFloat with % (other fall back to SupportsInt)
FLOAT_TYPES: Final = {"e", "E", "f", "F", "g", "G"}

class ConversionSpecifier:
    def __init__(
        self, match: Match[str], start_pos: int = -1, non_standard_format_spec: bool = False
    ) -> None:
        self.whole_seq = match.group()
        self.start_pos = start_pos

        m_dict = match.groupdict()
        self.key = m_dict.get("key")

        # Replace unmatched optional groups with empty matches (for convenience).
        self.conv_type = m_dict.get("type", "")
        self.flags = m_dict.get("flags", "")
        self.width = m_dict.get("width", "")
        self.precision = m_dict.get("precision", "")

        # Used only for str.format() calls (it may be custom for types with __format__()).
        self.format_spec = m_dict.get("format_spec")
        self.non_standard_format_spec = non_standard_format_spec
        # Used only for str.format() calls.
        self.conversion = m_dict.get("conversion")
        # Full formatted expression (i.e. key plus following attributes and/or indexes).
        # Used only for str.format() calls.
        self.field = m_dict.get("field")

    def has_key(self) -> bool:
        return self.key is not None

    def has_star(self) -> bool:
        return self.width == "*" or self.precision == "*"

def parse_conversion_specifiers(format_str: str) -> list[ConversionSpecifier]:
    """Parse c-printf-style format string into list of conversion specifiers."""
    specifiers: list[ConversionSpecifier] = []
    for m in re.finditer(FORMAT_RE, format_str):
        specifiers.append(ConversionSpecifier(m, start_pos=m.start()))
    return specifiers

def parse_format_value(
    format_value: str, ctx: Context, msg: MessageBuilder, nested: bool = False
) -> list[ConversionSpecifier] | None:
    """Parse format string into list of conversion specifiers.

    The specifiers may be nested (two levels maximum), in this case they are ordered as
    '{0:{1}}, {2:{3}{4}}'. Return None in case of an error.
    """
    top_targets = find_non_escaped_targets(format_value, ctx, msg)
    if top_targets is None:
        return None

    result: list[ConversionSpecifier] = []
    for target, start_pos in top_targets:
        match = FORMAT_RE_NEW.fullmatch(target)
        if match:
            conv_spec = ConversionSpecifier(match, start_pos=start_pos)
        else:
            custom_match = FORMAT_RE_NEW_CUSTOM.fullmatch(target)
            if custom_match:
                conv_spec = ConversionSpecifier(
                    custom_match, start_pos=start_pos, non_standard_format_spec=True
                )
            else:
                msg.fail(
                    "Invalid conversion specifier in format string",
                    ctx,
                    code=codes.STRING_FORMATTING,
                )
                return None

        if conv_spec.key and ("{" in conv_spec.key or "}" in conv_spec.key):
            msg.fail("Conversion value must not contain { or }", ctx, code=codes.STRING_FORMATTING)
            return None
        result.append(conv_spec)

        # Parse nested conversions that are allowed in format specifier.
        if (
            conv_spec.format_spec
            and conv_spec.non_standard_format_spec
            and ("{" in conv_spec.format_spec or "}" in conv_spec.format_spec)
        ):
            if nested:
                msg.fail(
                    "Formatting nesting must be at most two levels deep",
                    ctx,
                    code=codes.STRING_FORMATTING,
                )
                return None
            sub_conv_specs = parse_format_value(conv_spec.format_spec, ctx, msg, nested=True)
            if sub_conv_specs is None:
                return None
            result.extend(sub_conv_specs)
    return result

def find_non_escaped_targets(
    format_value: str, ctx: Context, msg: MessageBuilder
) -> list[tuple[str, int]] | None:
    """Return list of raw (un-parsed) format specifiers in format string.

    Format specifiers don't include enclosing braces. We don't use regexp for
    this because they don't work well with nested/repeated patterns
    (both greedy and non-greedy), and these are heavily used internally for
    representation of f-strings.

    Return None in case of an error.
    """
    result = []
    next_spec = ""
    pos = 0
    nesting = 0
    while pos < len(format_value):
        c = format_value[pos]
        if not nesting:
            # Skip any paired '{{' and '}}', enter nesting on '{', report error on '}'.
            if c == "{":
                if pos < len(format_value) - 1 and format_value[pos + 1] == "{":
                    pos += 1
                else:
                    nesting = 1
            if c == "}":
                if pos < len(format_value) - 1 and format_value[pos + 1] == "}":
                    pos += 1
                else:
                    msg.fail(
                        "Invalid conversion specifier in format string: unexpected }",
                        ctx,
                        code=codes.STRING_FORMATTING,
                    )
                    return None
        else:
            # Adjust nesting level, then either continue adding chars or move on.
            if c == "{":
                nesting += 1
            if c == "}":
                nesting -= 1
            if nesting:
                next_spec += c
            else:
                result.append((next_spec, pos - len(next_spec)))
                next_spec = ""
        pos += 1
    if nesting:
        msg.fail(
            "Invalid conversion specifier in format string: unmatched {",
            ctx,
            code=codes.STRING_FORMATTING,
        )
        return None
    return result

class StringFormatterChecker:
    """String interpolation/formatter type checker.

    This class works closely together with checker.ExpressionChecker.
    """

    # Some services are provided by a TypeChecker instance.
    chk: mypy.checker.TypeChecker
    # This is shared with TypeChecker, but stored also here for convenience.
    msg: MessageBuilder
    # Some services are provided by a ExpressionChecker instance.
    exprchk: mypy.checkexpr.ExpressionChecker

    def __init__(
        self,
        exprchk: mypy.checkexpr.ExpressionChecker,
        chk: mypy.checker.TypeChecker,
        msg: MessageBuilder,
    ) -> None:
        """Construct an expression type checker."""
        self.chk = chk
        self.exprchk = exprchk
        self.msg = msg

    def check_str_format_call(self, call: CallExpr, format_value: str) -> None:
        """Perform more precise checks for str.format() calls when possible.

        Currently the checks are performed for:
          * Actual string literals
          * Literal types with string values
          * Final names with string values

        The checks that we currently perform:
          * Check generic validity (e.g. unmatched { or }, and {} in invalid positions)
          * Check consistency of specifiers' auto-numbering
          * Verify that replacements can be found for all conversion specifiers,
            and all arguments were used
          * Non-standard format specs are only allowed for types with custom __format__
          * Type check replacements with accessors applied (if any).
          * Verify that specifier type is known and matches replacement type
          * Perform special checks for some specifier types:
            - 'c' requires a single character string
            - 's' must not accept bytes
            - non-empty flags are only allowed for numeric types
        """
        conv_specs = parse_format_value(format_value, call, self.msg)
        if conv_specs is None:
            return
        if not self.auto_generate_keys(conv_specs, call):
            return
        self.check_specs_in_format_call(call, conv_specs, format_value)

    def check_specs_in_format_call(
        self, call: CallExpr, specs: list[ConversionSpecifier], format_value: str
    ) -> None:
        """Perform pairwise checks for conversion specifiers vs their replacements.

        The core logic for format checking is implemented in this method.
        """
        assert all(s.key for s in specs), "Keys must be auto-generated first!"
        replacements = self.find_replacements_in_call(call, [cast(str, s.key) for s in specs])
        assert len(replacements) == len(specs)
        for spec, repl in zip(specs, replacements):
            repl = self.apply_field_accessors(spec, repl, ctx=call)
            actual_type = repl.type if isinstance(repl, TempNode) else self.chk.lookup_type(repl)
            assert actual_type is not None

            # Special case custom formatting.
            if (
                spec.format_spec
                and spec.non_standard_format_spec
                and
                # Exclude "dynamic" specifiers (i.e. containing nested formatting).
                not ("{" in spec.format_spec or "}" in spec.format_spec)
            ):
                if (
                    not custom_special_method(actual_type, "__format__", check_all=True)
                    or spec.conversion
                ):
                    # TODO: add support for some custom specs like datetime?
                    self.msg.fail(
                        f'Unrecognized format specification "{spec.format_spec[1:]}"',
                        call,
                        code=codes.STRING_FORMATTING,
                    )
                    continue
            # Adjust expected and actual types.
            if not spec.conv_type:
                expected_type: Type | None = AnyType(TypeOfAny.special_form)
            else:
                assert isinstance(call.callee, MemberExpr)
                if isinstance(call.callee.expr, StrExpr):
                    format_str = call.callee.expr
                else:
                    format_str = StrExpr(format_value)
                expected_type = self.conversion_type(
                    spec.conv_type, call, format_str, format_call=True
                )
            if spec.conversion is not None:
                # If the explicit conversion is given, then explicit conversion is called _first_.
                if spec.conversion[1] not in "rsa":
                    self.msg.fail(
                        'Invalid conversion type "{}",'
                        ' must be one of "r", "s" or "a"'.format(spec.conversion[1]),
                        call,
                        code=codes.STRING_FORMATTING,
                    )
                actual_type = self.named_type("builtins.str")

            # Perform the checks for given types.
            if expected_type is None:
                continue

            a_type = get_proper_type(actual_type)
            actual_items = (
                get_proper_types(a_type.items) if isinstance(a_type, UnionType) else [a_type]
            )
            for a_type in actual_items:
                if custom_special_method(a_type, "__format__"):
                    continue
                self.check_placeholder_type(a_type, expected_type, call)
                self.perform_special_format_checks(spec, call, repl, a_type, expected_type)

    def perform_special_format_checks(
        self,
        spec: ConversionSpecifier,
        call: CallExpr,
        repl: Expression,
        actual_type: Type,
        expected_type: Type,
    ) -> None:
        # TODO: try refactoring to combine this logic with % formatting.
        if spec.conv_type == "c":
            if isinstance(repl, (StrExpr, BytesExpr)) and len(repl.value) != 1:
                self.msg.requires_int_or_char(call, format_call=True)
            c_typ = get_proper_type(self.chk.lookup_type(repl))
            if isinstance(c_typ, Instance) and c_typ.last_known_value:
                c_typ = c_typ.last_known_value
            if isinstance(c_typ, LiteralType) and isinstance(c_typ.value, str):
                if len(c_typ.value) != 1:
                    self.msg.requires_int_or_char(call, format_call=True)
        if (not spec.conv_type or spec.conv_type == "s") and not spec.conversion:
            if has_type_component(actual_type, "builtins.bytes") and not custom_special_method(
                actual_type, "__str__"
            ):
                self.msg.fail(
                    'If x = b\'abc\' then f"{x}" or "{}".format(x) produces "b\'abc\'", '
                    'not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). '
                    "Otherwise, decode the bytes",
                    call,
                    code=codes.STR_BYTES_PY3,
                )
        if spec.flags:
            numeric_types = UnionType(
                [self.named_type("builtins.int"), self.named_type("builtins.float")]
            )
            if (
                spec.conv_type
                and spec.conv_type not in NUMERIC_TYPES_NEW
                or not spec.conv_type
                and not is_subtype(actual_type, numeric_types)
                and not custom_special_method(actual_type, "__format__")
            ):
                self.msg.fail(
                    "Numeric flags are only allowed for numeric types",
                    call,
                    code=codes.STRING_FORMATTING,
                )

    def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Expression]:
        """Find replacement expression for every specifier in str.format() call.

        In case of an error use TempNode(AnyType).
        """
        result: list[Expression] = []
        used: set[Expression] = set()
        for key in keys:
            if key.isdecimal():
                expr = self.get_expr_by_position(int(key), call)
                if not expr:
                    self.msg.fail(
                        "Cannot find replacement for positional"
                        " format specifier {}".format(key),
                        call,
                        code=codes.STRING_FORMATTING,
                    )
                    expr = TempNode(AnyType(TypeOfAny.from_error))
            else:
                expr = self.get_expr_by_name(key, call)
                if not expr:
                    self.msg.fail(
                        f'Cannot find replacement for named format specifier "{key}"',
                        call,
                        code=codes.STRING_FORMATTING,
                    )
                    expr = TempNode(AnyType(TypeOfAny.from_error))
            result.append(expr)
            if not isinstance(expr, TempNode):
                used.add(expr)
        # Strictly speaking not using all replacements is not a type error, but most likely
        # a typo in user code, so we show an error like we do for % formatting.
        total_explicit = len([kind for kind in call.arg_kinds if kind in (ARG_POS, ARG_NAMED)])
        if len(used) < total_explicit:
            self.msg.too_many_string_formatting_arguments(call)
        return result

    def get_expr_by_position(self, pos: int, call: CallExpr) -> Expression | None:
        """Get positional replacement expression from '{0}, {1}'.format(x, y, ...) call.

        If the type is from *args, return TempNode(<item type>). Return None in case of
        an error.
        """
        pos_args = [arg for arg, kind in zip(call.args, call.arg_kinds) if kind == ARG_POS]
        if pos < len(pos_args):
            return pos_args[pos]
        star_args = [arg for arg, kind in zip(call.args, call.arg_kinds) if kind == ARG_STAR]
        if not star_args:
            return None

        # Fall back to *args when present in call.
        star_arg = star_args[0]
        varargs_type = get_proper_type(self.chk.lookup_type(star_arg))
        if not isinstance(varargs_type, Instance) or not varargs_type.type.has_base(
            "typing.Sequence"
        ):
            # Error should be already reported.
            return TempNode(AnyType(TypeOfAny.special_form))
        iter_info = self.chk.named_generic_type(
            "typing.Sequence", [AnyType(TypeOfAny.special_form)]
        ).type
        return TempNode(map_instance_to_supertype(varargs_type, iter_info).args[0])

    def get_expr_by_name(self, key: str, call: CallExpr) -> Expression | None:
        """Get named replacement expression from '{name}'.format(name=...) call.

        If the type is from **kwargs, return TempNode(<item type>). Return None in case of
        an error.
        """
        named_args = [
            arg
            for arg, kind, name in zip(call.args, call.arg_kinds, call.arg_names)
            if kind == ARG_NAMED and name == key
        ]
        if named_args:
            return named_args[0]
        star_args_2 = [arg for arg, kind in zip(call.args, call.arg_kinds) if kind == ARG_STAR2]
        if not star_args_2:
            return None
        star_arg_2 = star_args_2[0]
        kwargs_type = get_proper_type(self.chk.lookup_type(star_arg_2))
        if not isinstance(kwargs_type, Instance) or not kwargs_type.type.has_base(
            "typing.Mapping"
        ):
            # Error should be already reported.
            return TempNode(AnyType(TypeOfAny.special_form))
        any_type = AnyType(TypeOfAny.special_form)
        mapping_info = self.chk.named_generic_type("typing.Mapping", [any_type, any_type]).type
        return TempNode(map_instance_to_supertype(kwargs_type, mapping_info).args[1])

    def auto_generate_keys(self, all_specs: list[ConversionSpecifier], ctx: Context) -> bool:
        """Translate '{} {name} {}' to '{0} {name} {1}'.

        Return True if generation was successful, otherwise report an error and return false.
        """
        some_defined = any(s.key and s.key.isdecimal() for s in all_specs)
        all_defined = all(bool(s.key) for s in all_specs)
        if some_defined and not all_defined:
            self.msg.fail(
                "Cannot combine automatic field numbering and manual field specification",
                ctx,
                code=codes.STRING_FORMATTING,
            )
            return False
        if all_defined:
            return True
        next_index = 0
        for spec in all_specs:
            if not spec.key:
                str_index = str(next_index)
                spec.key = str_index
                # Update also the full field (i.e. turn {.x} into {0.x}).
                if not spec.field:
                    spec.field = str_index
                else:
                    spec.field = str_index + spec.field
                next_index += 1
        return True

    def apply_field_accessors(
        self, spec: ConversionSpecifier, repl: Expression, ctx: Context
    ) -> Expression:
        """Transform and validate expr in '{.attr[item]}'.format(expr) into expr.attr['item'].

        If validation fails, return TempNode(AnyType).
        """
        assert spec.key, "Keys must be auto-generated first!"
        if spec.field == spec.key:
            return repl
        assert spec.field

        temp_errors = Errors(self.chk.options)
        dummy = DUMMY_FIELD_NAME + spec.field[len(spec.key) :]
        temp_ast: Node = parse(
            dummy, fnam="<format>", module=None, options=self.chk.options, errors=temp_errors
        )
        if temp_errors.is_errors():
            self.msg.fail(
                f'Syntax error in format specifier "{spec.field}"',
                ctx,
                code=codes.STRING_FORMATTING,
            )
            return TempNode(AnyType(TypeOfAny.from_error))

        # These asserts are guaranteed by the original regexp.
        assert isinstance(temp_ast, MypyFile)
        temp_ast = temp_ast.defs[0]
        assert isinstance(temp_ast, ExpressionStmt)
        temp_ast = temp_ast.expr
        if not self.validate_and_transform_accessors(temp_ast, repl, spec, ctx=ctx):
            return TempNode(AnyType(TypeOfAny.from_error))

        # Check if there are any other errors (like missing members).
        # TODO: fix column to point to actual start of the format specifier _within_ string.
        temp_ast.line = ctx.line
        temp_ast.column = ctx.column
        self.exprchk.accept(temp_ast)
        return temp_ast

    def validate_and_transform_accessors(
        self,
        temp_ast: Expression,
        original_repl: Expression,
        spec: ConversionSpecifier,
        ctx: Context,
    ) -> bool:
        """Validate and transform (in-place) format field accessors.

        On error, report it and return False. The transformations include replacing the dummy
        variable with actual replacement expression and translating any name expressions in an
        index into strings, so that this will work:

            class User(TypedDict):
                name: str
                id: int
            u: User
            '{[id]:d} -> {[name]}'.format(u)
        """
        if not isinstance(temp_ast, (MemberExpr, IndexExpr)):
            self.msg.fail(
                "Only index and member expressions are allowed in"
                ' format field accessors; got "{}"'.format(spec.field),
                ctx,
                code=codes.STRING_FORMATTING,
            )
            return False
        if isinstance(temp_ast, MemberExpr):
            node = temp_ast.expr
        else:
            node = temp_ast.base
            if not isinstance(temp_ast.index, (NameExpr, IntExpr)):
                assert spec.key, "Call this method only after auto-generating keys!"
                assert spec.field
                self.msg.fail(
                    "Invalid index expression in format field"
                    ' accessor "{}"'.format(spec.field[len(spec.key) :]),
                    ctx,
                    code=codes.STRING_FORMATTING,
                )
                return False
            if isinstance(temp_ast.index, NameExpr):
                temp_ast.index = StrExpr(temp_ast.index.name)
        if isinstance(node, NameExpr) and node.name == DUMMY_FIELD_NAME:
            # Replace it with the actual replacement expression.
            assert isinstance(temp_ast, (IndexExpr, MemberExpr))  # XXX: this is redundant
            if isinstance(temp_ast, IndexExpr):
                temp_ast.base = original_repl
            else:
                temp_ast.expr = original_repl
            return True
        node.line = ctx.line
        node.column = ctx.column
        return self.validate_and_transform_accessors(
            node, original_repl=original_repl, spec=spec, ctx=ctx
        )

    # TODO: In Python 3, the bytes formatting has a more restricted set of options
    #       compared to string formatting.
    def check_str_interpolation(self, expr: FormatStringExpr, replacements: Expression) -> Type:
        """Check the types of the 'replacements' in a string interpolation
        expression: str % replacements.
        """
        self.exprchk.accept(expr)
        specifiers = parse_conversion_specifiers(expr.value)
        has_mapping_keys = self.analyze_conversion_specifiers(specifiers, expr)
        if has_mapping_keys is None:
            pass  # Error was reported
        elif has_mapping_keys:
            self.check_mapping_str_interpolation(specifiers, replacements, expr)
        else:
            self.check_simple_str_interpolation(specifiers, replacements, expr)

        if isinstance(expr, BytesExpr):
            return self.named_type("builtins.bytes")
        elif isinstance(expr, StrExpr):
            return self.named_type("builtins.str")
        else:
            assert False

    def analyze_conversion_specifiers(
        self, specifiers: list[ConversionSpecifier], context: Context
    ) -> bool | None:
        has_star = any(specifier.has_star() for specifier in specifiers)
        has_key = any(specifier.has_key() for specifier in specifiers)
        all_have_keys = all(
            specifier.has_key() or specifier.conv_type == "%" for specifier in specifiers
        )

        if has_key and has_star:
            self.msg.string_interpolation_with_star_and_key(context)
            return None
        if has_key and not all_have_keys:
            self.msg.string_interpolation_mixing_key_and_non_keys(context)
            return None
        return has_key

    def check_simple_str_interpolation(
        self,
        specifiers: list[ConversionSpecifier],
        replacements: Expression,
        expr: FormatStringExpr,
    ) -> None:
        """Check % string interpolation with positional specifiers '%s, %d' % ('yes, 42')."""
        checkers = self.build_replacement_checkers(specifiers, replacements, expr)
        if checkers is None:
            return

        rhs_type = get_proper_type(self.accept(replacements))
        rep_types: list[Type] = []
        if isinstance(rhs_type, TupleType):
            rep_types = rhs_type.items
            unpack_index = find_unpack_in_list(rep_types)
            if unpack_index is not None:
                # TODO: we should probably warn about potentially short tuple.
                # However, without special-casing for tuple(f(i) for in other_tuple)
                # this causes false positive on mypy self-check in report.py.
                extras = max(0, len(checkers) - len(rep_types) + 1)
                unpacked = rep_types[unpack_index]
                assert isinstance(unpacked, UnpackType)
                unpacked = get_proper_type(unpacked.type)
                if isinstance(unpacked, TypeVarTupleType):
                    unpacked = get_proper_type(unpacked.upper_bound)
                assert (
                    isinstance(unpacked, Instance) and unpacked.type.fullname == "builtins.tuple"
                )
                unpack_items = [unpacked.args[0]] * extras
                rep_types = rep_types[:unpack_index] + unpack_items + rep_types[unpack_index + 1 :]
        elif isinstance(rhs_type, AnyType):
            return
        elif isinstance(rhs_type, Instance) and rhs_type.type.fullname == "builtins.tuple":
            # Assume that an arbitrary-length tuple has the right number of items.
            rep_types = [rhs_type.args[0]] * len(checkers)
        elif isinstance(rhs_type, UnionType):
            for typ in rhs_type.relevant_items():
                temp_node = TempNode(typ)
                temp_node.line = replacements.line
                self.check_simple_str_interpolation(specifiers, temp_node, expr)
            return
        else:
            rep_types = [rhs_type]

        if len(checkers) > len(rep_types):
            # Only check the fix-length Tuple type. Other Iterable types would skip.
            if is_subtype(rhs_type, self.chk.named_type("typing.Iterable")) and not isinstance(
                rhs_type, TupleType
            ):
                return
            else:
                self.msg.too_few_string_formatting_arguments(replacements)
        elif len(checkers) < len(rep_types):
            self.msg.too_many_string_formatting_arguments(replacements)
        else:
            if len(checkers) == 1:
                check_node, check_type = checkers[0]
                if isinstance(rhs_type, TupleType) and len(rhs_type.items) == 1:
                    check_type(rhs_type.items[0])
                else:
                    check_node(replacements)
            elif isinstance(replacements, TupleExpr) and not any(
                isinstance(item, StarExpr) for item in replacements.items
            ):
                for checks, rep_node in zip(checkers, replacements.items):
                    check_node, check_type = checks
                    check_node(rep_node)
            else:
                for checks, rep_type in zip(checkers, rep_types):
                    check_node, check_type = checks
                    check_type(rep_type)

    def check_mapping_str_interpolation(
        self,
        specifiers: list[ConversionSpecifier],
        replacements: Expression,
        expr: FormatStringExpr,
    ) -> None:
        """Check % string interpolation with names specifiers '%(name)s' % {'name': 'John'}."""
        if isinstance(replacements, DictExpr) and all(
            isinstance(k, (StrExpr, BytesExpr)) for k, v in replacements.items
        ):
            mapping: dict[str, Type] = {}
            for k, v in replacements.items:
                if isinstance(expr, BytesExpr):
                    # Special case: for bytes formatting keys must be bytes.
                    if not isinstance(k, BytesExpr):
                        self.msg.fail(
                            "Dictionary keys in bytes formatting must be bytes, not strings",
                            expr,
                            code=codes.STRING_FORMATTING,
                        )
                key_str = cast(FormatStringExpr, k).value
                mapping[key_str] = self.accept(v)

            for specifier in specifiers:
                if specifier.conv_type == "%":
                    # %% is allowed in mappings, no checking is required
                    continue
                assert specifier.key is not None
                if specifier.key not in mapping:
                    self.msg.key_not_in_mapping(specifier.key, replacements)
                    return
                rep_type = mapping[specifier.key]
                assert specifier.conv_type is not None
                expected_type = self.conversion_type(specifier.conv_type, replacements, expr)
                if expected_type is None:
                    return
                self.chk.check_subtype(
                    rep_type,
                    expected_type,
                    replacements,
                    message_registry.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION,
                    "expression has type",
                    f"placeholder with key '{specifier.key}' has type",
                    code=codes.STRING_FORMATTING,
                )
                if specifier.conv_type == "s":
                    self.check_s_special_cases(expr, rep_type, expr)
        else:
            rep_type = self.accept(replacements)
            dict_type = self.build_dict_type(expr)
            self.chk.check_subtype(
                rep_type,
                dict_type,
                replacements,
                message_registry.FORMAT_REQUIRES_MAPPING,
                "expression has type",
                "expected type for mapping is",
                code=codes.STRING_FORMATTING,
            )

    def build_dict_type(self, expr: FormatStringExpr) -> Type:
        """Build expected mapping type for right operand in % formatting."""
        any_type = AnyType(TypeOfAny.special_form)
        if isinstance(expr, BytesExpr):
            bytes_type = self.chk.named_generic_type("builtins.bytes", [])
            return self.chk.named_generic_type(
                "_typeshed.SupportsKeysAndGetItem", [bytes_type, any_type]
            )
        elif isinstance(expr, StrExpr):
            str_type = self.chk.named_generic_type("builtins.str", [])
            return self.chk.named_generic_type(
                "_typeshed.SupportsKeysAndGetItem", [str_type, any_type]
            )
        else:
            assert False, "Unreachable"

    def build_replacement_checkers(
        self, specifiers: list[ConversionSpecifier], context: Context, expr: FormatStringExpr
    ) -> list[Checkers] | None:
        checkers: list[Checkers] = []
        for specifier in specifiers:
            checker = self.replacement_checkers(specifier, context, expr)
            if checker is None:
                return None
            checkers.extend(checker)
        return checkers

    def replacement_checkers(
        self, specifier: ConversionSpecifier, context: Context, expr: FormatStringExpr
    ) -> list[Checkers] | None:
        """Returns a list of tuples of two functions that check whether a replacement is
        of the right type for the specifier. The first function takes a node and checks
        its type in the right type context. The second function just checks a type.
        """
        checkers: list[Checkers] = []

        if specifier.width == "*":
            checkers.append(self.checkers_for_star(context))
        if specifier.precision == "*":
            checkers.append(self.checkers_for_star(context))

        if specifier.conv_type == "c":
            c = self.checkers_for_c_type(specifier.conv_type, context, expr)
            if c is None:
                return None
            checkers.append(c)
        elif specifier.conv_type is not None and specifier.conv_type != "%":
            c = self.checkers_for_regular_type(specifier.conv_type, context, expr)
            if c is None:
                return None
            checkers.append(c)
        return checkers

    def checkers_for_star(self, context: Context) -> Checkers:
        """Returns a tuple of check functions that check whether, respectively,
        a node or a type is compatible with a star in a conversion specifier.
        """
        expected = self.named_type("builtins.int")

        def check_type(type: Type) -> bool:
            expected = self.named_type("builtins.int")
            return self.chk.check_subtype(
                type, expected, context, "* wants int", code=codes.STRING_FORMATTING
            )

        def check_expr(expr: Expression) -> None:
            type = self.accept(expr, expected)
            check_type(type)

        return check_expr, check_type

    def check_placeholder_type(self, typ: Type, expected_type: Type, context: Context) -> bool:
        return self.chk.check_subtype(
            typ,
            expected_type,
            context,
            message_registry.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION,
            "expression has type",
            "placeholder has type",
            code=codes.STRING_FORMATTING,
        )

    def checkers_for_regular_type(
        self, conv_type: str, context: Context, expr: FormatStringExpr
    ) -> Checkers | None:
        """Returns a tuple of check functions that check whether, respectively,
        a node or a type is compatible with 'type'. Return None in case of an error.
        """
        expected_type = self.conversion_type(conv_type, context, expr)
        if expected_type is None:
            return None

        def check_type(typ: Type) -> bool:
            assert expected_type is not None
            ret = self.check_placeholder_type(typ, expected_type, context)
            if ret and conv_type == "s":
                ret = self.check_s_special_cases(expr, typ, context)
            return ret

        def check_expr(expr: Expression) -> None:
            type = self.accept(expr, expected_type)
            check_type(type)

        return check_expr, check_type

    def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Context) -> bool:
        """Additional special cases for %s in bytes vs string context."""
        if isinstance(expr, StrExpr):
            # Couple special cases for string formatting.
            if has_type_component(typ, "builtins.bytes"):
                self.msg.fail(
                    'If x = b\'abc\' then "%s" % x produces "b\'abc\'", not "abc". '
                    'If this is desired behavior use "%r" % x. Otherwise, decode the bytes',
                    context,
                    code=codes.STR_BYTES_PY3,
                )
                return False
        if isinstance(expr, BytesExpr):
            # A special case for bytes formatting: b'%s' actually requires bytes on Python 3.
            if has_type_component(typ, "builtins.str"):
                self.msg.fail(
                    "On Python 3 b'%s' requires bytes, not string",
                    context,
                    code=codes.STRING_FORMATTING,
                )
                return False
        return True

    def checkers_for_c_type(
        self, type: str, context: Context, format_expr: FormatStringExpr
    ) -> Checkers | None:
        """Returns a tuple of check functions that check whether, respectively,
        a node or a type is compatible with 'type' that is a character type.
        """
        expected_type = self.conversion_type(type, context, format_expr)
        if expected_type is None:
            return None

        def check_type(type: Type) -> bool:
            assert expected_type is not None
            if isinstance(format_expr, BytesExpr):
                err_msg = '"%c" requires an integer in range(256) or a single byte'
            else:
                err_msg = '"%c" requires int or char'
            return self.chk.check_subtype(
                type,
                expected_type,
                context,
                err_msg,
                "expression has type",
                code=codes.STRING_FORMATTING,
            )

        def check_expr(expr: Expression) -> None:
            """int, or str with length 1"""
            type = self.accept(expr, expected_type)
            # We need further check with expr to make sure that
            # it has exact one char or one single byte.
            if check_type(type):
                # Python 3 doesn't support b'%c' % str
                if (
                    isinstance(format_expr, BytesExpr)
                    and isinstance(expr, BytesExpr)
                    and len(expr.value) != 1
                ):
                    self.msg.requires_int_or_single_byte(context)
                elif isinstance(expr, (StrExpr, BytesExpr)) and len(expr.value) != 1:
                    self.msg.requires_int_or_char(context)

        return check_expr, check_type

    def conversion_type(
        self, p: str, context: Context, expr: FormatStringExpr, format_call: bool = False
    ) -> Type | None:
        """Return the type that is accepted for a string interpolation conversion specifier type.

        Note that both Python's float (e.g. %f) and integer (e.g. %d)
        specifier types accept both float and integers.

        The 'format_call' argument indicates whether this type came from % interpolation or from
        a str.format() call, the meaning of few formatting types are different.
        """
        NUMERIC_TYPES = NUMERIC_TYPES_NEW if format_call else NUMERIC_TYPES_OLD
        INT_TYPES = REQUIRE_INT_NEW if format_call else REQUIRE_INT_OLD
        if p == "b" and not format_call:
            if not isinstance(expr, BytesExpr):
                self.msg.fail(
                    'Format character "b" is only supported on bytes patterns',
                    context,
                    code=codes.STRING_FORMATTING,
                )
                return None
            return self.named_type("builtins.bytes")
        elif p == "a":
            # TODO: return type object?
            return AnyType(TypeOfAny.special_form)
        elif p in ["s", "r"]:
            return AnyType(TypeOfAny.special_form)
        elif p in NUMERIC_TYPES:
            if p in INT_TYPES:
                numeric_types = [self.named_type("builtins.int")]
            else:
                numeric_types = [
                    self.named_type("builtins.int"),
                    self.named_type("builtins.float"),
                ]
                if not format_call:
                    if p in FLOAT_TYPES:
                        numeric_types.append(self.named_type("typing.SupportsFloat"))
                    else:
                        numeric_types.append(self.named_type("typing.SupportsInt"))
            return UnionType.make_union(numeric_types)
        elif p in ["c"]:
            if isinstance(expr, BytesExpr):
                return UnionType(
                    [self.named_type("builtins.int"), self.named_type("builtins.bytes")]
                )
            else:
                return UnionType(
                    [self.named_type("builtins.int"), self.named_type("builtins.str")]
                )
        else:
            self.msg.unsupported_placeholder(p, context)
            return None

    #
    # Helpers
    #

    def named_type(self, name: str) -> Instance:
        """Return an instance type with type given by the name and no type
        arguments. Alias for TypeChecker.named_type.
        """
        return self.chk.named_type(name)

    def accept(self, expr: Expression, context: Type | None = None) -> Type:
        """Type check a node. Alias for TypeChecker.accept."""
        return self.chk.expr_checker.accept(expr, context)

def has_type_component(typ: Type, fullname: str) -> bool:
    """Is this a specific instance type, or a union that contains it?

    We use this ad-hoc function instead of a proper visitor or subtype check
    because some str vs bytes errors are strictly speaking not runtime errors,
    but rather highly counter-intuitive behavior. This is similar to what is used for
    --strict-equality.
    """
    typ = get_proper_type(typ)
    if isinstance(typ, Instance):
        return typ.type.has_base(fullname)
    elif isinstance(typ, TypeVarType):
        return has_type_component(typ.upper_bound, fullname) or any(
            has_type_component(v, fullname) for v in typ.values
        )
    elif isinstance(typ, UnionType):
        return any(has_type_component(t, fullname) for t in typ.relevant_items())
    return False


from __future__ import annotations

import argparse
import configparser
import glob as fileglob
import os
import re
import sys
from io import StringIO

from mypy.errorcodes import error_codes

if sys.version_info >= (3, 11):
    import tomllib
else:
    import tomli as tomllib

from typing import (
    Any,
    Callable,
    Dict,
    Final,
    Iterable,
    List,
    Mapping,
    MutableMapping,
    Sequence,
    TextIO,
    Tuple,
    Union,
)
from typing_extensions import TypeAlias as _TypeAlias

from mypy import defaults
from mypy.options import PER_MODULE_OPTIONS, Options

_CONFIG_VALUE_TYPES: _TypeAlias = Union[
    str, bool, int, float, Dict[str, str], List[str], Tuple[int, int]
]
_INI_PARSER_CALLABLE: _TypeAlias = Callable[[Any], _CONFIG_VALUE_TYPES]

def parse_version(v: str | float) -> tuple[int, int]:
    m = re.match(r"\A(\d)\.(\d+)\Z", str(v))
    if not m:
        raise argparse.ArgumentTypeError(f"Invalid python version '{v}' (expected format: 'x.y')")
    major, minor = int(m.group(1)), int(m.group(2))
    if major == 2 and minor == 7:
        pass  # Error raised elsewhere
    elif major == 3:
        if minor < defaults.PYTHON3_VERSION_MIN[1]:
            msg = "Python 3.{} is not supported (must be {}.{} or higher)".format(
                minor, *defaults.PYTHON3_VERSION_MIN
            )

            if isinstance(v, float):
                msg += ". You may need to put quotes around your Python version"

            raise argparse.ArgumentTypeError(msg)
    else:
        raise argparse.ArgumentTypeError(
            f"Python major version '{major}' out of range (must be 3)"
        )
    return major, minor

def try_split(v: str | Sequence[str], split_regex: str = "[,]") -> list[str]:
    """Split and trim a str or list of str into a list of str"""
    if isinstance(v, str):
        return [p.strip() for p in re.split(split_regex, v)]

    return [p.strip() for p in v]

def validate_codes(codes: list[str]) -> list[str]:
    invalid_codes = set(codes) - set(error_codes.keys())
    if invalid_codes:
        raise argparse.ArgumentTypeError(
            f"Invalid error code(s): {', '.join(sorted(invalid_codes))}"
        )
    return codes

def validate_package_allow_list(allow_list: list[str]) -> list[str]:
    for p in allow_list:
        msg = f"Invalid allow list entry: {p}"
        if "*" in p:
            raise argparse.ArgumentTypeError(
                f"{msg} (entries are already prefixes so must not contain *)"
            )
        if "\\" in p or "/" in p:
            raise argparse.ArgumentTypeError(
                f"{msg} (entries must be packages like foo.bar not directories or files)"
            )
    return allow_list

def expand_path(path: str) -> str:
    """Expand the user home directory and any environment variables contained within
    the provided path.
    """

    return os.path.expandvars(os.path.expanduser(path))

def str_or_array_as_list(v: str | Sequence[str]) -> list[str]:
    if isinstance(v, str):
        return [v.strip()] if v.strip() else []
    return [p.strip() for p in v if p.strip()]

def split_and_match_files_list(paths: Sequence[str]) -> list[str]:
    """Take a list of files/directories (with support for globbing through the glob library).

    Where a path/glob matches no file, we still include the raw path in the resulting list.

    Returns a list of file paths
    """
    expanded_paths = []

    for path in paths:
        path = expand_path(path.strip())
        globbed_files = fileglob.glob(path, recursive=True)
        if globbed_files:
            expanded_paths.extend(globbed_files)
        else:
            expanded_paths.append(path)

    return expanded_paths

def split_and_match_files(paths: str) -> list[str]:
    """Take a string representing a list of files/directories (with support for globbing
    through the glob library).

    Where a path/glob matches no file, we still include the raw path in the resulting list.

    Returns a list of file paths
    """

    return split_and_match_files_list(paths.split(","))

def check_follow_imports(choice: str) -> str:
    choices = ["normal", "silent", "skip", "error"]
    if choice not in choices:
        raise argparse.ArgumentTypeError(
            "invalid choice '{}' (choose from {})".format(
                choice, ", ".join(f"'{x}'" for x in choices)
            )
        )
    return choice

def check_junit_format(choice: str) -> str:
    choices = ["global", "per_file"]
    if choice not in choices:
        raise argparse.ArgumentTypeError(
            "invalid choice '{}' (choose from {})".format(
                choice, ", ".join(f"'{x}'" for x in choices)
            )
        )
    return choice

def split_commas(value: str) -> list[str]:
    # Uses a bit smarter technique to allow last trailing comma
    # and to remove last `""` item from the split.
    items = value.split(",")
    if items and items[-1] == "":
        items.pop(-1)
    return items

# For most options, the type of the default value set in options.py is
# sufficient, and we don't have to do anything here.  This table
# exists to specify types for values initialized to None or container
# types.
ini_config_types: Final[dict[str, _INI_PARSER_CALLABLE]] = {
    "python_version": parse_version,
    "custom_typing_module": str,
    "custom_typeshed_dir": expand_path,
    "mypy_path": lambda s: [expand_path(p.strip()) for p in re.split("[,:]", s)],
    "files": split_and_match_files,
    "quickstart_file": expand_path,
    "junit_xml": expand_path,
    "junit_format": check_junit_format,
    "follow_imports": check_follow_imports,
    "no_site_packages": bool,
    "plugins": lambda s: [p.strip() for p in split_commas(s)],
    "always_true": lambda s: [p.strip() for p in split_commas(s)],
    "always_false": lambda s: [p.strip() for p in split_commas(s)],
    "untyped_calls_exclude": lambda s: validate_package_allow_list(
        [p.strip() for p in split_commas(s)]
    ),
    "enable_incomplete_feature": lambda s: [p.strip() for p in split_commas(s)],
    "disable_error_code": lambda s: validate_codes([p.strip() for p in split_commas(s)]),
    "enable_error_code": lambda s: validate_codes([p.strip() for p in split_commas(s)]),
    "package_root": lambda s: [p.strip() for p in split_commas(s)],
    "cache_dir": expand_path,
    "python_executable": expand_path,
    "strict": bool,
    "exclude": lambda s: [s.strip()],
    "packages": try_split,
    "modules": try_split,
}

# Reuse the ini_config_types and overwrite the diff
toml_config_types: Final[dict[str, _INI_PARSER_CALLABLE]] = ini_config_types.copy()
toml_config_types.update(
    {
        "python_version": parse_version,
        "mypy_path": lambda s: [expand_path(p) for p in try_split(s, "[,:]")],
        "files": lambda s: split_and_match_files_list(try_split(s)),
        "junit_format": lambda s: check_junit_format(str(s)),
        "follow_imports": lambda s: check_follow_imports(str(s)),
        "plugins": try_split,
        "always_true": try_split,
        "always_false": try_split,
        "untyped_calls_exclude": lambda s: validate_package_allow_list(try_split(s)),
        "enable_incomplete_feature": try_split,
        "disable_error_code": lambda s: validate_codes(try_split(s)),
        "enable_error_code": lambda s: validate_codes(try_split(s)),
        "package_root": try_split,
        "exclude": str_or_array_as_list,
        "packages": try_split,
        "modules": try_split,
    }
)

def parse_config_file(
    options: Options,
    set_strict_flags: Callable[[], None],
    filename: str | None,
    stdout: TextIO | None = None,
    stderr: TextIO | None = None,
) -> None:
    """Parse a config file into an Options object.

    Errors are written to stderr but are not fatal.

    If filename is None, fall back to default config files.
    """
    stdout = stdout or sys.stdout
    stderr = stderr or sys.stderr

    if filename is not None:
        config_files: tuple[str, ...] = (filename,)
    else:
        config_files_iter: Iterable[str] = map(os.path.expanduser, defaults.CONFIG_FILES)
        config_files = tuple(config_files_iter)

    config_parser = configparser.RawConfigParser()

    for config_file in config_files:
        if not os.path.exists(config_file):
            continue
        try:
            if is_toml(config_file):
                with open(config_file, "rb") as f:
                    toml_data = tomllib.load(f)
                # Filter down to just mypy relevant toml keys
                toml_data = toml_data.get("tool", {})
                if "mypy" not in toml_data:
                    continue
                toml_data = {"mypy": toml_data["mypy"]}
                parser: MutableMapping[str, Any] = destructure_overrides(toml_data)
                config_types = toml_config_types
            else:
                config_parser.read(config_file)
                parser = config_parser
                config_types = ini_config_types
        except (tomllib.TOMLDecodeError, configparser.Error, ConfigTOMLValueError) as err:
            print(f"{config_file}: {err}", file=stderr)
        else:
            if config_file in defaults.SHARED_CONFIG_FILES and "mypy" not in parser:
                continue
            file_read = config_file
            options.config_file = file_read
            break
    else:
        return

    os.environ["MYPY_CONFIG_FILE_DIR"] = os.path.dirname(os.path.abspath(config_file))

    if "mypy" not in parser:
        if filename or file_read not in defaults.SHARED_CONFIG_FILES:
            print(f"{file_read}: No [mypy] section in config file", file=stderr)
    else:
        section = parser["mypy"]
        prefix = f"{file_read}: [mypy]: "
        updates, report_dirs = parse_section(
            prefix, options, set_strict_flags, section, config_types, stderr
        )
        for k, v in updates.items():
            setattr(options, k, v)
        options.report_dirs.update(report_dirs)

    for name, section in parser.items():
        if name.startswith("mypy-"):
            prefix = get_prefix(file_read, name)
            updates, report_dirs = parse_section(
                prefix, options, set_strict_flags, section, config_types, stderr
            )
            if report_dirs:
                print(
                    prefix,
                    "Per-module sections should not specify reports ({})".format(
                        ", ".join(s + "_report" for s in sorted(report_dirs))
                    ),
                    file=stderr,
                )
            if set(updates) - PER_MODULE_OPTIONS:
                print(
                    prefix,
                    "Per-module sections should only specify per-module flags ({})".format(
                        ", ".join(sorted(set(updates) - PER_MODULE_OPTIONS))
                    ),
                    file=stderr,
                )
                updates = {k: v for k, v in updates.items() if k in PER_MODULE_OPTIONS}

            globs = name[5:]
            for glob in globs.split(","):
                # For backwards compatibility, replace (back)slashes with dots.
                glob = glob.replace(os.sep, ".")
                if os.altsep:
                    glob = glob.replace(os.altsep, ".")

                if any(c in glob for c in "?[]!") or any(
                    "*" in x and x != "*" for x in glob.split(".")
                ):
                    print(
                        prefix,
                        "Patterns must be fully-qualified module names, optionally "
                        "with '*' in some components (e.g spam.*.eggs.*)",
                        file=stderr,
                    )
                else:
                    options.per_module_options[glob] = updates

def get_prefix(file_read: str, name: str) -> str:
    if is_toml(file_read):
        module_name_str = 'module = "%s"' % "-".join(name.split("-")[1:])
    else:
        module_name_str = name

    return f"{file_read}: [{module_name_str}]:"

def is_toml(filename: str) -> bool:
    return filename.lower().endswith(".toml")

def destructure_overrides(toml_data: dict[str, Any]) -> dict[str, Any]:
    """Take the new [[tool.mypy.overrides]] section array in the pyproject.toml file,
    and convert it back to a flatter structure that the existing config_parser can handle.

    E.g. the following pyproject.toml file:

        [[tool.mypy.overrides]]
        module = [
            "a.b",
            "b.*"
        ]
        disallow_untyped_defs = true

        [[tool.mypy.overrides]]
        module = 'c'
        disallow_untyped_defs = false

    Would map to the following config dict that it would have gotten from parsing an equivalent
    ini file:

        {
            "mypy-a.b": {
                disallow_untyped_defs = true,
            },
            "mypy-b.*": {
                disallow_untyped_defs = true,
            },
            "mypy-c": {
                disallow_untyped_defs: false,
            },
        }
    """
    if "overrides" not in toml_data["mypy"]:
        return toml_data

    if not isinstance(toml_data["mypy"]["overrides"], list):
        raise ConfigTOMLValueError(
            "tool.mypy.overrides sections must be an array. Please make "
            "sure you are using double brackets like so: [[tool.mypy.overrides]]"
        )

    result = toml_data.copy()
    for override in result["mypy"]["overrides"]:
        if "module" not in override:
            raise ConfigTOMLValueError(
                "toml config file contains a [[tool.mypy.overrides]] "
                "section, but no module to override was specified."
            )

        if isinstance(override["module"], str):
            modules = [override["module"]]
        elif isinstance(override["module"], list):
            modules = override["module"]
        else:
            raise ConfigTOMLValueError(
                "toml config file contains a [[tool.mypy.overrides]] "
                "section with a module value that is not a string or a list of "
                "strings"
            )

        for module in modules:
            module_overrides = override.copy()
            del module_overrides["module"]
            old_config_name = f"mypy-{module}"
            if old_config_name not in result:
                result[old_config_name] = module_overrides
            else:
                for new_key, new_value in module_overrides.items():
                    if (
                        new_key in result[old_config_name]
                        and result[old_config_name][new_key] != new_value
                    ):
                        raise ConfigTOMLValueError(
                            "toml config file contains "
                            "[[tool.mypy.overrides]] sections with conflicting "
                            f"values. Module '{module}' has two different values for '{new_key}'"
                        )
                    result[old_config_name][new_key] = new_value

    del result["mypy"]["overrides"]
    return result

def parse_section(
    prefix: str,
    template: Options,
    set_strict_flags: Callable[[], None],
    section: Mapping[str, Any],
    config_types: dict[str, Any],
    stderr: TextIO = sys.stderr,
) -> tuple[dict[str, object], dict[str, str]]:
    """Parse one section of a config file.

    Returns a dict of option values encountered, and a dict of report directories.
    """
    results: dict[str, object] = {}
    report_dirs: dict[str, str] = {}

    # Because these fields exist on Options, without proactive checking, we would accept them
    # and crash later
    invalid_options = {
        "enabled_error_codes": "enable_error_code",
        "disabled_error_codes": "disable_error_code",
    }

    for key in section:
        invert = False
        options_key = key
        if key in config_types:
            ct = config_types[key]
        elif key in invalid_options:
            print(
                f"{prefix}Unrecognized option: {key} = {section[key]}"
                f" (did you mean {invalid_options[key]}?)",
                file=stderr,
            )
            continue
        else:
            dv = None
            # We have to keep new_semantic_analyzer in Options
            # for plugin compatibility but it is not a valid option anymore.
            assert hasattr(template, "new_semantic_analyzer")
            if key != "new_semantic_analyzer":
                dv = getattr(template, key, None)
            if dv is None:
                if key.endswith("_report"):
                    report_type = key[:-7].replace("_", "-")
                    if report_type in defaults.REPORTER_NAMES:
                        report_dirs[report_type] = str(section[key])
                    else:
                        print(f"{prefix}Unrecognized report type: {key}", file=stderr)
                    continue
                if key.startswith("x_"):
                    pass  # Don't complain about `x_blah` flags
                elif key.startswith("no_") and hasattr(template, key[3:]):
                    options_key = key[3:]
                    invert = True
                elif key.startswith("allow") and hasattr(template, "dis" + key):
                    options_key = "dis" + key
                    invert = True
                elif key.startswith("disallow") and hasattr(template, key[3:]):
                    options_key = key[3:]
                    invert = True
                elif key.startswith("show_") and hasattr(template, "hide_" + key[5:]):
                    options_key = "hide_" + key[5:]
                    invert = True
                elif key == "strict":
                    pass  # Special handling below
                else:
                    print(f"{prefix}Unrecognized option: {key} = {section[key]}", file=stderr)
                if invert:
                    dv = getattr(template, options_key, None)
                else:
                    continue
            ct = type(dv)
        v: Any = None
        try:
            if ct is bool:
                if isinstance(section, dict):
                    v = convert_to_boolean(section.get(key))
                else:
                    v = section.getboolean(key)  # type: ignore[attr-defined]  # Until better stub
                if invert:
                    v = not v
            elif callable(ct):
                if invert:
                    print(f"{prefix}Can not invert non-boolean key {options_key}", file=stderr)
                    continue
                try:
                    v = ct(section.get(key))
                except argparse.ArgumentTypeError as err:
                    print(f"{prefix}{key}: {err}", file=stderr)
                    continue
            else:
                print(f"{prefix}Don't know what type {key} should have", file=stderr)
                continue
        except ValueError as err:
            print(f"{prefix}{key}: {err}", file=stderr)
            continue
        if key == "strict":
            if v:
                set_strict_flags()
            continue
        results[options_key] = v

    # These two flags act as per-module overrides, so store the empty defaults.
    if "disable_error_code" not in results:
        results["disable_error_code"] = []
    if "enable_error_code" not in results:
        results["enable_error_code"] = []

    return results, report_dirs

def convert_to_boolean(value: Any | None) -> bool:
    """Return a boolean value translating from other types if necessary."""
    if isinstance(value, bool):
        return value
    if not isinstance(value, str):
        value = str(value)
    if value.lower() not in configparser.RawConfigParser.BOOLEAN_STATES:
        raise ValueError(f"Not a boolean: {value}")
    return configparser.RawConfigParser.BOOLEAN_STATES[value.lower()]

def split_directive(s: str) -> tuple[list[str], list[str]]:
    """Split s on commas, except during quoted sections.

    Returns the parts and a list of error messages."""
    parts = []
    cur: list[str] = []
    errors = []
    i = 0
    while i < len(s):
        if s[i] == ",":
            parts.append("".join(cur).strip())
            cur = []
        elif s[i] == '"':
            i += 1
            while i < len(s) and s[i] != '"':
                cur.append(s[i])
                i += 1
            if i == len(s):
                errors.append("Unterminated quote in configuration comment")
                cur.clear()
        else:
            cur.append(s[i])
        i += 1
    if cur:
        parts.append("".join(cur).strip())

    return parts, errors

def mypy_comments_to_config_map(line: str, template: Options) -> tuple[dict[str, str], list[str]]:
    """Rewrite the mypy comment syntax into ini file syntax."""
    options = {}
    entries, errors = split_directive(line)
    for entry in entries:
        if "=" not in entry:
            name = entry
            value = None
        else:
            name, value = (x.strip() for x in entry.split("=", 1))

        name = name.replace("-", "_")
        if value is None:
            value = "True"
        options[name] = value

    return options, errors

def parse_mypy_comments(
    args: list[tuple[int, str]], template: Options
) -> tuple[dict[str, object], list[tuple[int, str]]]:
    """Parse a collection of inline mypy: configuration comments.

    Returns a dictionary of options to be applied and a list of error messages
    generated.
    """

    errors: list[tuple[int, str]] = []
    sections = {}

    for lineno, line in args:
        # In order to easily match the behavior for bools, we abuse configparser.
        # Oddly, the only way to get the SectionProxy object with the getboolean
        # method is to create a config parser.
        parser = configparser.RawConfigParser()
        options, parse_errors = mypy_comments_to_config_map(line, template)
        parser["dummy"] = options
        errors.extend((lineno, x) for x in parse_errors)

        stderr = StringIO()
        strict_found = False

        def set_strict_flags() -> None:
            nonlocal strict_found
            strict_found = True

        new_sections, reports = parse_section(
            "", template, set_strict_flags, parser["dummy"], ini_config_types, stderr=stderr
        )
        errors.extend((lineno, x) for x in stderr.getvalue().strip().split("\n") if x)
        if reports:
            errors.append((lineno, "Reports not supported in inline configuration"))
        if strict_found:
            errors.append(
                (
                    lineno,
                    'Setting "strict" not supported in inline configuration: specify it in '
                    "a configuration file instead, or set individual inline flags "
                    '(see "mypy -h" for the list of flags enabled in strict mode)',
                )
            )

        sections.update(new_sections)

    return sections, errors

def get_config_module_names(filename: str | None, modules: list[str]) -> str:
    if not filename or not modules:
        return ""

    if not is_toml(filename):
        return ", ".join(f"[mypy-{module}]" for module in modules)

    return "module = ['%s']" % ("', '".join(sorted(modules)))

class ConfigTOMLValueError(ValueError):
    pass


"""Constant folding of expressions.

For example, 3 + 5 can be constant folded into 8.
"""

from __future__ import annotations

from typing import Final, Union

from mypy.nodes import (
    ComplexExpr,
    Expression,
    FloatExpr,
    IntExpr,
    NameExpr,
    OpExpr,
    StrExpr,
    UnaryExpr,
    Var,
)

# All possible result types of constant folding
ConstantValue = Union[int, bool, float, complex, str]
CONST_TYPES: Final = (int, bool, float, complex, str)

def constant_fold_expr(expr: Expression, cur_mod_id: str) -> ConstantValue | None:
    """Return the constant value of an expression for supported operations.

    Among other things, support int arithmetic and string
    concatenation. For example, the expression 3 + 5 has the constant
    value 8.

    Also bind simple references to final constants defined in the
    current module (cur_mod_id). Binding to references is best effort
    -- we don't bind references to other modules. Mypyc trusts these
    to be correct in compiled modules, so that it can replace a
    constant expression (or a reference to one) with the statically
    computed value. We don't want to infer constant values based on
    stubs, in particular, as these might not match the implementation
    (due to version skew, for example).

    Return None if unsuccessful.
    """
    if isinstance(expr, IntExpr):
        return expr.value
    if isinstance(expr, StrExpr):
        return expr.value
    if isinstance(expr, FloatExpr):
        return expr.value
    if isinstance(expr, ComplexExpr):
        return expr.value
    elif isinstance(expr, NameExpr):
        if expr.name == "True":
            return True
        elif expr.name == "False":
            return False
        node = expr.node
        if (
            isinstance(node, Var)
            and node.is_final
            and node.fullname.rsplit(".", 1)[0] == cur_mod_id
        ):
            value = node.final_value
            if isinstance(value, (CONST_TYPES)):
                return value
    elif isinstance(expr, OpExpr):
        left = constant_fold_expr(expr.left, cur_mod_id)
        right = constant_fold_expr(expr.right, cur_mod_id)
        if left is not None and right is not None:
            return constant_fold_binary_op(expr.op, left, right)
    elif isinstance(expr, UnaryExpr):
        value = constant_fold_expr(expr.expr, cur_mod_id)
        if value is not None:
            return constant_fold_unary_op(expr.op, value)
    return None

def constant_fold_binary_op(
    op: str, left: ConstantValue, right: ConstantValue
) -> ConstantValue | None:
    if isinstance(left, int) and isinstance(right, int):
        return constant_fold_binary_int_op(op, left, right)

    # Float and mixed int/float arithmetic.
    if isinstance(left, float) and isinstance(right, float):
        return constant_fold_binary_float_op(op, left, right)
    elif isinstance(left, float) and isinstance(right, int):
        return constant_fold_binary_float_op(op, left, right)
    elif isinstance(left, int) and isinstance(right, float):
        return constant_fold_binary_float_op(op, left, right)

    # String concatenation and multiplication.
    if op == "+" and isinstance(left, str) and isinstance(right, str):
        return left + right
    elif op == "*" and isinstance(left, str) and isinstance(right, int):
        return left * right
    elif op == "*" and isinstance(left, int) and isinstance(right, str):
        return left * right

    # Complex construction.
    if op == "+" and isinstance(left, (int, float)) and isinstance(right, complex):
        return left + right
    elif op == "+" and isinstance(left, complex) and isinstance(right, (int, float)):
        return left + right
    elif op == "-" and isinstance(left, (int, float)) and isinstance(right, complex):
        return left - right
    elif op == "-" and isinstance(left, complex) and isinstance(right, (int, float)):
        return left - right

    return None

def constant_fold_binary_int_op(op: str, left: int, right: int) -> int | float | None:
    if op == "+":
        return left + right
    if op == "-":
        return left - right
    elif op == "*":
        return left * right
    elif op == "/":
        if right != 0:
            return left / right
    elif op == "//":
        if right != 0:
            return left // right
    elif op == "%":
        if right != 0:
            return left % right
    elif op == "&":
        return left & right
    elif op == "|":
        return left | right
    elif op == "^":
        return left ^ right
    elif op == "<<":
        if right >= 0:
            return left << right
    elif op == ">>":
        if right >= 0:
            return left >> right
    elif op == "**":
        if right >= 0:
            ret = left**right
            assert isinstance(ret, int)
            return ret
    return None

def constant_fold_binary_float_op(op: str, left: int | float, right: int | float) -> float | None:
    assert not (isinstance(left, int) and isinstance(right, int)), (op, left, right)
    if op == "+":
        return left + right
    elif op == "-":
        return left - right
    elif op == "*":
        return left * right
    elif op == "/":
        if right != 0:
            return left / right
    elif op == "//":
        if right != 0:
            return left // right
    elif op == "%":
        if right != 0:
            return left % right
    elif op == "**":
        if (left < 0 and isinstance(right, int)) or left > 0:
            try:
                ret = left**right
            except OverflowError:
                return None
            else:
                assert isinstance(ret, float), ret
                return ret

    return None

def constant_fold_unary_op(op: str, value: ConstantValue) -> int | float | None:
    if op == "-" and isinstance(value, (int, float)):
        return -value
    elif op == "~" and isinstance(value, int):
        return ~value
    elif op == "+" and isinstance(value, (int, float)):
        return value
    return None