"""Shared definitions used by different parts of semantic analysis."""

from __future__ import annotations

from abc import abstractmethod
from typing import Callable, Final, overload
from typing_extensions import Literal, Protocol

from mypy_extensions import trait

from mypy.errorcodes import LITERAL_REQ, ErrorCode
from mypy.nodes import (
    CallExpr,
    ClassDef,
    Context,
    DataclassTransformSpec,
    Decorator,
    Expression,
    FuncDef,
    NameExpr,
    Node,
    OverloadedFuncDef,
    RefExpr,
    SymbolNode,
    SymbolTable,
    SymbolTableNode,
    TypeInfo,
)
from mypy.plugin import SemanticAnalyzerPluginInterface
from mypy.tvar_scope import TypeVarLikeScope
from mypy.type_visitor import ANY_STRATEGY, BoolTypeQuery
from mypy.typeops import make_simplified_union
from mypy.types import (
    TPDICT_FB_NAMES,
    AnyType,
    FunctionLike,
    Instance,
    Parameters,
    ParamSpecFlavor,
    ParamSpecType,
    PlaceholderType,
    ProperType,
    TupleType,
    Type,
    TypeOfAny,
    TypeVarId,
    TypeVarLikeType,
    TypeVarTupleType,
    UnpackType,
    get_proper_type,
)

# Subclasses can override these Var attributes with incompatible types. This can also be
# set for individual attributes using 'allow_incompatible_override' of Var.
ALLOW_INCOMPATIBLE_OVERRIDE: Final = ("__slots__", "__deletable__", "__match_args__")

# Priorities for ordering of patches within the "patch" phase of semantic analysis
# (after the main pass):

# Fix fallbacks (does subtype checks).
PRIORITY_FALLBACKS: Final = 1

@trait
class SemanticAnalyzerCoreInterface:
    """A core abstract interface to generic semantic analyzer functionality.

    This is implemented by both semantic analyzer passes 2 and 3.
    """

    @abstractmethod
    def lookup_qualified(
        self, name: str, ctx: Context, suppress_errors: bool = False
    ) -> SymbolTableNode | None:
        raise NotImplementedError

    @abstractmethod
    def lookup_fully_qualified(self, name: str) -> SymbolTableNode:
        raise NotImplementedError

    @abstractmethod
    def lookup_fully_qualified_or_none(self, name: str) -> SymbolTableNode | None:
        raise NotImplementedError

    @abstractmethod
    def fail(
        self,
        msg: str,
        ctx: Context,
        serious: bool = False,
        *,
        blocker: bool = False,
        code: ErrorCode | None = None,
    ) -> None:
        raise NotImplementedError

    @abstractmethod
    def note(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None:
        raise NotImplementedError

    @abstractmethod
    def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool:
        raise NotImplementedError

    @abstractmethod
    def record_incomplete_ref(self) -> None:
        raise NotImplementedError

    @abstractmethod
    def defer(self, debug_context: Context | None = None, force_progress: bool = False) -> None:
        raise NotImplementedError

    @abstractmethod
    def is_incomplete_namespace(self, fullname: str) -> bool:
        """Is a module or class namespace potentially missing some definitions?"""
        raise NotImplementedError

    @property
    @abstractmethod
    def final_iteration(self) -> bool:
        """Is this the final iteration of semantic analysis?"""
        raise NotImplementedError

    @abstractmethod
    def is_future_flag_set(self, flag: str) -> bool:
        """Is the specific __future__ feature imported"""
        raise NotImplementedError

    @property
    @abstractmethod
    def is_stub_file(self) -> bool:
        raise NotImplementedError

    @abstractmethod
    def is_func_scope(self) -> bool:
        raise NotImplementedError

    @property
    @abstractmethod
    def type(self) -> TypeInfo | None:
        raise NotImplementedError

@trait
class SemanticAnalyzerInterface(SemanticAnalyzerCoreInterface):
    """A limited abstract interface to some generic semantic analyzer pass 2 functionality.

    We use this interface for various reasons:

    * Looser coupling
    * Cleaner import graph
    * Less need to pass around callback functions
    """

    tvar_scope: TypeVarLikeScope

    @abstractmethod
    def lookup(
        self, name: str, ctx: Context, suppress_errors: bool = False
    ) -> SymbolTableNode | None:
        raise NotImplementedError

    @abstractmethod
    def named_type(self, fullname: str, args: list[Type] | None = None) -> Instance:
        raise NotImplementedError

    @abstractmethod
    def named_type_or_none(self, fullname: str, args: list[Type] | None = None) -> Instance | None:
        raise NotImplementedError

    @abstractmethod
    def accept(self, node: Node) -> None:
        raise NotImplementedError

    @abstractmethod
    def anal_type(
        self,
        t: Type,
        *,
        tvar_scope: TypeVarLikeScope | None = None,
        allow_tuple_literal: bool = False,
        allow_unbound_tvars: bool = False,
        allow_typed_dict_special_forms: bool = False,
        allow_placeholder: bool = False,
        report_invalid_types: bool = True,
        prohibit_self_type: str | None = None,
    ) -> Type | None:
        raise NotImplementedError

    @abstractmethod
    def get_and_bind_all_tvars(self, type_exprs: list[Expression]) -> list[TypeVarLikeType]:
        raise NotImplementedError

    @abstractmethod
    def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance, line: int) -> TypeInfo:
        raise NotImplementedError

    @abstractmethod
    def schedule_patch(self, priority: int, fn: Callable[[], None]) -> None:
        raise NotImplementedError

    @abstractmethod
    def add_symbol_table_node(self, name: str, stnode: SymbolTableNode) -> bool:
        """Add node to the current symbol table."""
        raise NotImplementedError

    @abstractmethod
    def current_symbol_table(self) -> SymbolTable:
        """Get currently active symbol table.

        May be module, class, or local namespace.
        """
        raise NotImplementedError

    @abstractmethod
    def add_symbol(
        self,
        name: str,
        node: SymbolNode,
        context: Context,
        module_public: bool = True,
        module_hidden: bool = False,
        can_defer: bool = True,
    ) -> bool:
        """Add symbol to the current symbol table."""
        raise NotImplementedError

    @abstractmethod
    def add_symbol_skip_local(self, name: str, node: SymbolNode) -> None:
        """Add symbol to the current symbol table, skipping locals.

        This is used to store symbol nodes in a symbol table that
        is going to be serialized (local namespaces are not serialized).
        See implementation docstring for more details.
        """
        raise NotImplementedError

    @abstractmethod
    def parse_bool(self, expr: Expression) -> bool | None:
        raise NotImplementedError

    @abstractmethod
    def qualified_name(self, n: str) -> str:
        raise NotImplementedError

    @property
    @abstractmethod
    def is_typeshed_stub_file(self) -> bool:
        raise NotImplementedError

    @abstractmethod
    def process_placeholder(
        self, name: str | None, kind: str, ctx: Context, force_progress: bool = False
    ) -> None:
        raise NotImplementedError

def set_callable_name(sig: Type, fdef: FuncDef) -> ProperType:
    sig = get_proper_type(sig)
    if isinstance(sig, FunctionLike):
        if fdef.info:
            if fdef.info.fullname in TPDICT_FB_NAMES:
                # Avoid exposing the internal _TypedDict name.
                class_name = "TypedDict"
            else:
                class_name = fdef.info.name
            return sig.with_name(f"{fdef.name} of {class_name}")
        else:
            return sig.with_name(fdef.name)
    else:
        return sig

def calculate_tuple_fallback(typ: TupleType) -> None:
    """Calculate a precise item type for the fallback of a tuple type.

    This must be called only after the main semantic analysis pass, since joins
    aren't available before that.

    Note that there is an apparent chicken and egg problem with respect
    to verifying type arguments against bounds. Verifying bounds might
    require fallbacks, but we might use the bounds to calculate the
    fallbacks. In practice this is not a problem, since the worst that
    can happen is that we have invalid type argument values, and these
    can happen in later stages as well (they will generate errors, but
    we don't prevent their existence).
    """
    fallback = typ.partial_fallback
    assert fallback.type.fullname == "builtins.tuple"
    items = []
    for item in typ.items:
        # TODO: this duplicates some logic in typeops.tuple_fallback().
        if isinstance(item, UnpackType):
            unpacked_type = get_proper_type(item.type)
            if isinstance(unpacked_type, TypeVarTupleType):
                unpacked_type = get_proper_type(unpacked_type.upper_bound)
            if (
                isinstance(unpacked_type, Instance)
                and unpacked_type.type.fullname == "builtins.tuple"
            ):
                items.append(unpacked_type.args[0])
            else:
                raise NotImplementedError
        else:
            items.append(item)
    fallback.args = (make_simplified_union(items),)

class _NamedTypeCallback(Protocol):
    def __call__(self, fully_qualified_name: str, args: list[Type] | None = None) -> Instance: ...

def paramspec_args(
    name: str,
    fullname: str,
    id: TypeVarId,
    *,
    named_type_func: _NamedTypeCallback,
    line: int = -1,
    column: int = -1,
    prefix: Parameters | None = None,
) -> ParamSpecType:
    return ParamSpecType(
        name,
        fullname,
        id,
        flavor=ParamSpecFlavor.ARGS,
        upper_bound=named_type_func("builtins.tuple", [named_type_func("builtins.object")]),
        default=AnyType(TypeOfAny.from_omitted_generics),
        line=line,
        column=column,
        prefix=prefix,
    )

def paramspec_kwargs(
    name: str,
    fullname: str,
    id: TypeVarId,
    *,
    named_type_func: _NamedTypeCallback,
    line: int = -1,
    column: int = -1,
    prefix: Parameters | None = None,
) -> ParamSpecType:
    return ParamSpecType(
        name,
        fullname,
        id,
        flavor=ParamSpecFlavor.KWARGS,
        upper_bound=named_type_func(
            "builtins.dict", [named_type_func("builtins.str"), named_type_func("builtins.object")]
        ),
        default=AnyType(TypeOfAny.from_omitted_generics),
        line=line,
        column=column,
        prefix=prefix,
    )

class HasPlaceholders(BoolTypeQuery):
    def __init__(self) -> None:
        super().__init__(ANY_STRATEGY)

    def visit_placeholder_type(self, t: PlaceholderType) -> bool:
        return True

def has_placeholder(typ: Type) -> bool:
    """Check if a type contains any placeholder types (recursively)."""
    return typ.accept(HasPlaceholders())

def find_dataclass_transform_spec(node: Node | None) -> DataclassTransformSpec | None:
    """
    Find the dataclass transform spec for the given node, if any exists.

    Per PEP 681 (https://peps.python.org/pep-0681/#the-dataclass-transform-decorator), dataclass
    transforms can be specified in multiple ways, including decorator functions and
    metaclasses/base classes. This function resolves the spec from any of these variants.
    """

    # The spec only lives on the function/class definition itself, so we need to unwrap down to that
    # point
    if isinstance(node, CallExpr):
        # Like dataclasses.dataclass, transform-based decorators can be applied either with or
        # without parameters; ie, both of these forms are accepted:
        #
        # @typing.dataclass_transform
        # class Foo: ...
        # @typing.dataclass_transform(eq=True, order=True, ...)
        # class Bar: ...
        #
        # We need to unwrap the call for the second variant.
        node = node.callee

    if isinstance(node, RefExpr):
        node = node.node

    if isinstance(node, Decorator):
        # typing.dataclass_transform usage must always result in a Decorator; it always uses the
        # `@dataclass_transform(...)` syntax and never `@dataclass_transform`
        node = node.func

    if isinstance(node, OverloadedFuncDef):
        # The dataclass_transform decorator may be attached to any single overload, so we must
        # search them all.
        # Note that using more than one decorator is undefined behavior, so we can just take the
        # first that we find.
        for candidate in node.items:
            spec = find_dataclass_transform_spec(candidate)
            if spec is not None:
                return spec
        return find_dataclass_transform_spec(node.impl)

    # For functions, we can directly consult the AST field for the spec
    if isinstance(node, FuncDef):
        return node.dataclass_transform_spec

    if isinstance(node, ClassDef):
        node = node.info
    if isinstance(node, TypeInfo):
        # Search all parent classes to see if any are decorated with `typing.dataclass_transform`
        for base in node.mro[1:]:
            if base.dataclass_transform_spec is not None:
                return base.dataclass_transform_spec

        # Check if there is a metaclass that is decorated with `typing.dataclass_transform`
        #
        # Note that PEP 681 only discusses using a metaclass that is directly decorated with
        # `typing.dataclass_transform`; subclasses thereof should be treated with dataclass
        # semantics rather than as transforms:
        #
        # > If dataclass_transform is applied to a class, dataclass-like semantics will be assumed
        # > for any class that directly or indirectly derives from the decorated class or uses the
        # > decorated class as a metaclass.
        #
        # The wording doesn't make this entirely explicit, but Pyright (the reference
        # implementation for this PEP) only handles directly-decorated metaclasses.
        metaclass_type = node.metaclass_type
        if metaclass_type is not None and metaclass_type.type.dataclass_transform_spec is not None:
            return metaclass_type.type.dataclass_transform_spec

    return None

# Never returns `None` if a default is given
@overload
def require_bool_literal_argument(
    api: SemanticAnalyzerInterface | SemanticAnalyzerPluginInterface,
    expression: Expression,
    name: str,
    default: Literal[True] | Literal[False],
) -> bool: ...

@overload
def require_bool_literal_argument(
    api: SemanticAnalyzerInterface | SemanticAnalyzerPluginInterface,
    expression: Expression,
    name: str,
    default: None = None,
) -> bool | None: ...

def require_bool_literal_argument(
    api: SemanticAnalyzerInterface | SemanticAnalyzerPluginInterface,
    expression: Expression,
    name: str,
    default: bool | None = None,
) -> bool | None:
    """Attempt to interpret an expression as a boolean literal, and fail analysis if we can't."""
    value = parse_bool(expression)
    if value is None:
        api.fail(
            f'"{name}" argument must be a True or False literal', expression, code=LITERAL_REQ
        )
        return default

    return value

def parse_bool(expr: Expression) -> bool | None:
    if isinstance(expr, NameExpr):
        if expr.fullname == "builtins.True":
            return True
        if expr.fullname == "builtins.False":
            return False
    return None


"""Verify properties of type arguments, like 'int' in C[int] being valid.

This must happen after semantic analysis since there can be placeholder
types until the end of semantic analysis, and these break various type
operations, including subtype checks.
"""

from __future__ import annotations

from typing import Callable

from mypy import errorcodes as codes, message_registry
from mypy.errorcodes import ErrorCode
from mypy.errors import Errors
from mypy.message_registry import INVALID_PARAM_SPEC_LOCATION, INVALID_PARAM_SPEC_LOCATION_NOTE
from mypy.messages import format_type
from mypy.mixedtraverser import MixedTraverserVisitor
from mypy.nodes import Block, ClassDef, Context, FakeInfo, FuncItem, MypyFile
from mypy.options import Options
from mypy.scope import Scope
from mypy.subtypes import is_same_type, is_subtype
from mypy.types import (
    AnyType,
    CallableType,
    Instance,
    Parameters,
    ParamSpecType,
    TupleType,
    Type,
    TypeAliasType,
    TypeOfAny,
    TypeVarLikeType,
    TypeVarTupleType,
    TypeVarType,
    UnboundType,
    UnpackType,
    flatten_nested_tuples,
    get_proper_type,
    get_proper_types,
    split_with_prefix_and_suffix,
)
from mypy.typevartuples import erased_vars

class TypeArgumentAnalyzer(MixedTraverserVisitor):
    def __init__(
        self,
        errors: Errors,
        options: Options,
        is_typeshed_file: bool,
        named_type: Callable[[str, list[Type]], Instance],
    ) -> None:
        super().__init__()
        self.errors = errors
        self.options = options
        self.is_typeshed_file = is_typeshed_file
        self.named_type = named_type
        self.scope = Scope()
        # Should we also analyze function definitions, or only module top-levels?
        self.recurse_into_functions = True
        # Keep track of the type aliases already visited. This is needed to avoid
        # infinite recursion on types like A = Union[int, List[A]].
        self.seen_aliases: set[TypeAliasType] = set()

    def visit_mypy_file(self, o: MypyFile) -> None:
        self.errors.set_file(o.path, o.fullname, scope=self.scope, options=self.options)
        with self.scope.module_scope(o.fullname):
            super().visit_mypy_file(o)

    def visit_func(self, defn: FuncItem) -> None:
        if not self.recurse_into_functions:
            return
        with self.scope.function_scope(defn):
            super().visit_func(defn)

    def visit_class_def(self, defn: ClassDef) -> None:
        with self.scope.class_scope(defn.info):
            super().visit_class_def(defn)

    def visit_block(self, o: Block) -> None:
        if not o.is_unreachable:
            super().visit_block(o)

    def visit_type_alias_type(self, t: TypeAliasType) -> None:
        super().visit_type_alias_type(t)
        if t in self.seen_aliases:
            # Avoid infinite recursion on recursive type aliases.
            # Note: it is fine to skip the aliases we have already seen in non-recursive
            # types, since errors there have already been reported.
            return
        self.seen_aliases.add(t)
        assert t.alias is not None, f"Unfixed type alias {t.type_ref}"
        is_error, is_invalid = self.validate_args(
            t.alias.name, tuple(t.args), t.alias.alias_tvars, t
        )
        if is_invalid:
            # If there is an arity error (e.g. non-Parameters used for ParamSpec etc.),
            # then it is safer to erase the arguments completely, to avoid crashes later.
            # TODO: can we move this logic to typeanal.py?
            t.args = erased_vars(t.alias.alias_tvars, TypeOfAny.from_error)
        if not is_error:
            # If there was already an error for the alias itself, there is no point in checking
            # the expansion, most likely it will result in the same kind of error.
            get_proper_type(t).accept(self)

    def visit_tuple_type(self, t: TupleType) -> None:
        t.items = flatten_nested_tuples(t.items)
        # We could also normalize Tuple[*tuple[X, ...]] -> tuple[X, ...] like in
        # expand_type() but we can't do this here since it is not a translator visitor,
        # and we need to return an Instance instead of TupleType.
        super().visit_tuple_type(t)

    def visit_callable_type(self, t: CallableType) -> None:
        super().visit_callable_type(t)
        t.normalize_trivial_unpack()

    def visit_instance(self, t: Instance) -> None:
        super().visit_instance(t)
        # Type argument counts were checked in the main semantic analyzer pass. We assume
        # that the counts are correct here.
        info = t.type
        if isinstance(info, FakeInfo):
            return  # https://github.com/python/mypy/issues/11079
        _, is_invalid = self.validate_args(info.name, t.args, info.defn.type_vars, t)
        if is_invalid:
            t.args = tuple(erased_vars(info.defn.type_vars, TypeOfAny.from_error))
        if t.type.fullname == "builtins.tuple" and len(t.args) == 1:
            # Normalize Tuple[*Tuple[X, ...], ...] -> Tuple[X, ...]
            arg = t.args[0]
            if isinstance(arg, UnpackType):
                unpacked = get_proper_type(arg.type)
                if isinstance(unpacked, Instance):
                    assert unpacked.type.fullname == "builtins.tuple"
                    t.args = unpacked.args

    def validate_args(
        self, name: str, args: tuple[Type, ...], type_vars: list[TypeVarLikeType], ctx: Context
    ) -> tuple[bool, bool]:
        if any(isinstance(v, TypeVarTupleType) for v in type_vars):
            prefix = next(i for (i, v) in enumerate(type_vars) if isinstance(v, TypeVarTupleType))
            tvt = type_vars[prefix]
            assert isinstance(tvt, TypeVarTupleType)
            start, middle, end = split_with_prefix_and_suffix(
                tuple(args), prefix, len(type_vars) - prefix - 1
            )
            args = start + (TupleType(list(middle), tvt.tuple_fallback),) + end

        is_error = False
        is_invalid = False
        for (i, arg), tvar in zip(enumerate(args), type_vars):
            if isinstance(tvar, TypeVarType):
                if isinstance(arg, ParamSpecType):
                    is_invalid = True
                    self.fail(
                        INVALID_PARAM_SPEC_LOCATION.format(format_type(arg, self.options)),
                        ctx,
                        code=codes.VALID_TYPE,
                    )
                    self.note(
                        INVALID_PARAM_SPEC_LOCATION_NOTE.format(arg.name),
                        ctx,
                        code=codes.VALID_TYPE,
                    )
                    continue
                if isinstance(arg, Parameters):
                    is_invalid = True
                    self.fail(
                        f"Cannot use {format_type(arg, self.options)} for regular type variable,"
                        " only for ParamSpec",
                        ctx,
                        code=codes.VALID_TYPE,
                    )
                    continue
                if tvar.values:
                    if isinstance(arg, TypeVarType):
                        if self.in_type_alias_expr:
                            # Type aliases are allowed to use unconstrained type variables
                            # error will be checked at substitution point.
                            continue
                        arg_values = arg.values
                        if not arg_values:
                            is_error = True
                            self.fail(
                                message_registry.INVALID_TYPEVAR_AS_TYPEARG.format(arg.name, name),
                                ctx,
                                code=codes.TYPE_VAR,
                            )
                            continue
                    else:
                        arg_values = [arg]
                    if self.check_type_var_values(name, arg_values, tvar.name, tvar.values, ctx):
                        is_error = True
                # Check against upper bound. Since it's object the vast majority of the time,
                # add fast path to avoid a potentially slow subtype check.
                upper_bound = tvar.upper_bound
                object_upper_bound = (
                    type(upper_bound) is Instance
                    and upper_bound.type.fullname == "builtins.object"
                )
                if not object_upper_bound and not is_subtype(arg, upper_bound):
                    if self.in_type_alias_expr and isinstance(arg, TypeVarType):
                        # Type aliases are allowed to use unconstrained type variables
                        # error will be checked at substitution point.
                        continue
                    is_error = True
                    self.fail(
                        message_registry.INVALID_TYPEVAR_ARG_BOUND.format(
                            format_type(arg, self.options),
                            name,
                            format_type(upper_bound, self.options),
                        ),
                        ctx,
                        code=codes.TYPE_VAR,
                    )
            elif isinstance(tvar, ParamSpecType):
                if not isinstance(
                    get_proper_type(arg), (ParamSpecType, Parameters, AnyType, UnboundType)
                ):
                    is_invalid = True
                    self.fail(
                        "Can only replace ParamSpec with a parameter types list or"
                        f" another ParamSpec, got {format_type(arg, self.options)}",
                        ctx,
                        code=codes.VALID_TYPE,
                    )
        if is_invalid:
            is_error = True
        return is_error, is_invalid

    def visit_unpack_type(self, typ: UnpackType) -> None:
        super().visit_unpack_type(typ)
        proper_type = get_proper_type(typ.type)
        if isinstance(proper_type, TupleType):
            return
        if isinstance(proper_type, TypeVarTupleType):
            return
        # TODO: this should probably be .has_base("builtins.tuple"), also elsewhere. This is
        # tricky however, since this needs map_instance_to_supertype() available in many places.
        if isinstance(proper_type, Instance) and proper_type.type.fullname == "builtins.tuple":
            return
        if not isinstance(proper_type, (UnboundType, AnyType)):
            # Avoid extra errors if there were some errors already. Also interpret plain Any
            # as tuple[Any, ...] (this is better for the code in type checker).
            self.fail(
                message_registry.INVALID_UNPACK.format(format_type(proper_type, self.options)),
                typ.type,
                code=codes.VALID_TYPE,
            )
        typ.type = self.named_type("builtins.tuple", [AnyType(TypeOfAny.from_error)])

    def check_type_var_values(
        self, name: str, actuals: list[Type], arg_name: str, valids: list[Type], context: Context
    ) -> bool:
        is_error = False
        for actual in get_proper_types(actuals):
            # We skip UnboundType here, since they may appear in defn.bases,
            # the error will be caught when visiting info.bases, that have bound type
            # variables.
            if not isinstance(actual, (AnyType, UnboundType)) and not any(
                is_same_type(actual, value) for value in valids
            ):
                is_error = True
                if len(actuals) > 1 or not isinstance(actual, Instance):
                    self.fail(
                        message_registry.INVALID_TYPEVAR_ARG_VALUE.format(name),
                        context,
                        code=codes.TYPE_VAR,
                    )
                else:
                    class_name = f'"{name}"'
                    actual_type_name = f'"{actual.type.name}"'
                    self.fail(
                        message_registry.INCOMPATIBLE_TYPEVAR_VALUE.format(
                            arg_name, class_name, actual_type_name
                        ),
                        context,
                        code=codes.TYPE_VAR,
                    )
        return is_error

    def fail(self, msg: str, context: Context, *, code: ErrorCode | None = None) -> None:
        self.errors.report(context.line, context.column, msg, code=code)

    def note(self, msg: str, context: Context, *, code: ErrorCode | None = None) -> None:
        self.errors.report(context.line, context.column, msg, severity="note", code=code)


"""Semantic analysis of TypedDict definitions."""

from __future__ import annotations

from typing import Final

from mypy import errorcodes as codes, message_registry
from mypy.errorcodes import ErrorCode
from mypy.expandtype import expand_type
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
from mypy.message_registry import TYPEDDICT_OVERRIDE_MERGE
from mypy.messages import MessageBuilder
from mypy.nodes import (
    ARG_NAMED,
    ARG_POS,
    AssignmentStmt,
    CallExpr,
    ClassDef,
    Context,
    DictExpr,
    EllipsisExpr,
    Expression,
    ExpressionStmt,
    IndexExpr,
    NameExpr,
    PassStmt,
    RefExpr,
    Statement,
    StrExpr,
    TempNode,
    TupleExpr,
    TypedDictExpr,
    TypeInfo,
)
from mypy.options import Options
from mypy.semanal_shared import (
    SemanticAnalyzerInterface,
    has_placeholder,
    require_bool_literal_argument,
)
from mypy.state import state
from mypy.typeanal import check_for_explicit_any, has_any_from_unimported_type
from mypy.types import (
    TPDICT_NAMES,
    AnyType,
    ReadOnlyType,
    RequiredType,
    Type,
    TypedDictType,
    TypeOfAny,
    TypeVarLikeType,
)

TPDICT_CLASS_ERROR: Final = (
    'Invalid statement in TypedDict definition; expected "field_name: field_type"'
)

class TypedDictAnalyzer:
    def __init__(
        self, options: Options, api: SemanticAnalyzerInterface, msg: MessageBuilder
    ) -> None:
        self.options = options
        self.api = api
        self.msg = msg

    def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | None]:
        """Analyze a class that may define a TypedDict.

        Assume that base classes have been analyzed already.

        Note: Unlike normal classes, we won't create a TypeInfo until
        the whole definition of the TypeDict (including the body and all
        key names and types) is complete.  This is mostly because we
        store the corresponding TypedDictType in the TypeInfo.

        Return (is this a TypedDict, new TypeInfo). Specifics:
         * If we couldn't finish due to incomplete reference anywhere in
           the definition, return (True, None).
         * If this is not a TypedDict, return (False, None).
        """
        possible = False
        for base_expr in defn.base_type_exprs:
            if isinstance(base_expr, CallExpr):
                base_expr = base_expr.callee
            if isinstance(base_expr, IndexExpr):
                base_expr = base_expr.base
            if isinstance(base_expr, RefExpr):
                self.api.accept(base_expr)
                if base_expr.fullname in TPDICT_NAMES or self.is_typeddict(base_expr):
                    possible = True
                    if isinstance(base_expr.node, TypeInfo) and base_expr.node.is_final:
                        err = message_registry.CANNOT_INHERIT_FROM_FINAL
                        self.fail(err.format(base_expr.node.name).value, defn, code=err.code)
        if not possible:
            return False, None
        existing_info = None
        if isinstance(defn.analyzed, TypedDictExpr):
            existing_info = defn.analyzed.info
        if (
            len(defn.base_type_exprs) == 1
            and isinstance(defn.base_type_exprs[0], RefExpr)
            and defn.base_type_exprs[0].fullname in TPDICT_NAMES
        ):
            # Building a new TypedDict
            fields, types, statements, required_keys, readonly_keys = (
                self.analyze_typeddict_classdef_fields(defn)
            )
            if fields is None:
                return True, None  # Defer
            if self.api.is_func_scope() and "@" not in defn.name:
                defn.name += "@" + str(defn.line)
            info = self.build_typeddict_typeinfo(
                defn.name, fields, types, required_keys, readonly_keys, defn.line, existing_info
            )
            defn.analyzed = TypedDictExpr(info)
            defn.analyzed.line = defn.line
            defn.analyzed.column = defn.column
            defn.defs.body = statements
            return True, info

        # Extending/merging existing TypedDicts
        typeddict_bases: list[Expression] = []
        typeddict_bases_set = set()
        for expr in defn.base_type_exprs:
            ok, maybe_type_info, _ = self.check_typeddict(expr, None, False)
            if ok and maybe_type_info is not None:
                # expr is a CallExpr
                info = maybe_type_info
                typeddict_bases_set.add(info.fullname)
                typeddict_bases.append(expr)
            elif isinstance(expr, RefExpr) and expr.fullname in TPDICT_NAMES:
                if "TypedDict" not in typeddict_bases_set:
                    typeddict_bases_set.add("TypedDict")
                else:
                    self.fail('Duplicate base class "TypedDict"', defn)
            elif isinstance(expr, RefExpr) and self.is_typeddict(expr):
                assert expr.fullname
                if expr.fullname not in typeddict_bases_set:
                    typeddict_bases_set.add(expr.fullname)
                    typeddict_bases.append(expr)
                else:
                    assert isinstance(expr.node, TypeInfo)
                    self.fail(f'Duplicate base class "{expr.node.name}"', defn)
            elif isinstance(expr, IndexExpr) and self.is_typeddict(expr.base):
                assert isinstance(expr.base, RefExpr)
                assert expr.base.fullname
                if expr.base.fullname not in typeddict_bases_set:
                    typeddict_bases_set.add(expr.base.fullname)
                    typeddict_bases.append(expr)
                else:
                    assert isinstance(expr.base.node, TypeInfo)
                    self.fail(f'Duplicate base class "{expr.base.node.name}"', defn)
            else:
                self.fail("All bases of a new TypedDict must be TypedDict types", defn)

        keys: list[str] = []
        types = []
        required_keys = set()
        readonly_keys = set()
        # Iterate over bases in reverse order so that leftmost base class' keys take precedence
        for base in reversed(typeddict_bases):
            self.add_keys_and_types_from_base(
                base, keys, types, required_keys, readonly_keys, defn
            )
        (new_keys, new_types, new_statements, new_required_keys, new_readonly_keys) = (
            self.analyze_typeddict_classdef_fields(defn, keys)
        )
        if new_keys is None:
            return True, None  # Defer
        keys.extend(new_keys)
        types.extend(new_types)
        required_keys.update(new_required_keys)
        readonly_keys.update(new_readonly_keys)
        info = self.build_typeddict_typeinfo(
            defn.name, keys, types, required_keys, readonly_keys, defn.line, existing_info
        )
        defn.analyzed = TypedDictExpr(info)
        defn.analyzed.line = defn.line
        defn.analyzed.column = defn.column
        defn.defs.body = new_statements
        return True, info

    def add_keys_and_types_from_base(
        self,
        base: Expression,
        keys: list[str],
        types: list[Type],
        required_keys: set[str],
        readonly_keys: set[str],
        ctx: Context,
    ) -> None:
        base_args: list[Type] = []
        if isinstance(base, RefExpr):
            assert isinstance(base.node, TypeInfo)
            info = base.node
        elif isinstance(base, IndexExpr):
            assert isinstance(base.base, RefExpr)
            assert isinstance(base.base.node, TypeInfo)
            info = base.base.node
            args = self.analyze_base_args(base, ctx)
            if args is None:
                return
            base_args = args
        else:
            assert isinstance(base, CallExpr)
            assert isinstance(base.analyzed, TypedDictExpr)
            info = base.analyzed.info

        assert info.typeddict_type is not None
        base_typed_dict = info.typeddict_type
        base_items = base_typed_dict.items
        valid_items = base_items.copy()

        # Always fix invalid bases to avoid crashes.
        tvars = info.defn.type_vars
        if len(base_args) != len(tvars):
            any_kind = TypeOfAny.from_omitted_generics
            if base_args:
                self.fail(f'Invalid number of type arguments for "{info.name}"', ctx)
                any_kind = TypeOfAny.from_error
            base_args = [AnyType(any_kind) for _ in tvars]

        with state.strict_optional_set(self.options.strict_optional):
            valid_items = self.map_items_to_base(valid_items, tvars, base_args)
        for key in base_items:
            if key in keys:
                self.fail(TYPEDDICT_OVERRIDE_MERGE.format(key), ctx)
        keys.extend(valid_items.keys())
        types.extend(valid_items.values())
        required_keys.update(base_typed_dict.required_keys)
        readonly_keys.update(base_typed_dict.readonly_keys)

    def analyze_base_args(self, base: IndexExpr, ctx: Context) -> list[Type] | None:
        """Analyze arguments of base type expressions as types.

        We need to do this, because normal base class processing happens after
        the TypedDict special-casing (plus we get a custom error message).
        """
        base_args = []
        if isinstance(base.index, TupleExpr):
            args = base.index.items
        else:
            args = [base.index]

        for arg_expr in args:
            try:
                type = expr_to_unanalyzed_type(arg_expr, self.options, self.api.is_stub_file)
            except TypeTranslationError:
                self.fail("Invalid TypedDict type argument", ctx)
                return None
            analyzed = self.api.anal_type(
                type,
                allow_typed_dict_special_forms=True,
                allow_placeholder=not self.api.is_func_scope(),
            )
            if analyzed is None:
                return None
            base_args.append(analyzed)
        return base_args

    def map_items_to_base(
        self, valid_items: dict[str, Type], tvars: list[TypeVarLikeType], base_args: list[Type]
    ) -> dict[str, Type]:
        """Map item types to how they would look in their base with type arguments applied.

        Note it is safe to use expand_type() during semantic analysis, because it should never
        (indirectly) call is_subtype().
        """
        mapped_items = {}
        for key in valid_items:
            type_in_base = valid_items[key]
            if not tvars:
                mapped_items[key] = type_in_base
                continue
            # TODO: simple zip can't be used for variadic types.
            mapped_items[key] = expand_type(
                type_in_base, {t.id: a for (t, a) in zip(tvars, base_args)}
            )
        return mapped_items

    def analyze_typeddict_classdef_fields(
        self, defn: ClassDef, oldfields: list[str] | None = None
    ) -> tuple[list[str] | None, list[Type], list[Statement], set[str], set[str]]:
        """Analyze fields defined in a TypedDict class definition.

        This doesn't consider inherited fields (if any). Also consider totality,
        if given.

        Return tuple with these items:
         * List of keys (or None if found an incomplete reference --> deferral)
         * List of types for each key
         * List of statements from defn.defs.body that are legally allowed to be a
           part of a TypedDict definition
         * Set of required keys
        """
        fields: list[str] = []
        types: list[Type] = []
        statements: list[Statement] = []
        for stmt in defn.defs.body:
            if not isinstance(stmt, AssignmentStmt):
                # Still allow pass or ... (for empty TypedDict's) and docstrings
                if isinstance(stmt, PassStmt) or (
                    isinstance(stmt, ExpressionStmt)
                    and isinstance(stmt.expr, (EllipsisExpr, StrExpr))
                ):
                    statements.append(stmt)
                else:
                    defn.removed_statements.append(stmt)
                    self.fail(TPDICT_CLASS_ERROR, stmt)
            elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr):
                # An assignment, but an invalid one.
                defn.removed_statements.append(stmt)
                self.fail(TPDICT_CLASS_ERROR, stmt)
            else:
                name = stmt.lvalues[0].name
                if name in (oldfields or []):
                    self.fail(f'Overwriting TypedDict field "{name}" while extending', stmt)
                if name in fields:
                    self.fail(f'Duplicate TypedDict key "{name}"', stmt)
                    continue
                # Append stmt, name, and type in this case...
                fields.append(name)
                statements.append(stmt)
                if stmt.unanalyzed_type is None:
                    types.append(AnyType(TypeOfAny.unannotated))
                else:
                    analyzed = self.api.anal_type(
                        stmt.unanalyzed_type,
                        allow_typed_dict_special_forms=True,
                        allow_placeholder=not self.api.is_func_scope(),
                        prohibit_self_type="TypedDict item type",
                    )
                    if analyzed is None:
                        return None, [], [], set(), set()  # Need to defer
                    types.append(analyzed)
                    if not has_placeholder(analyzed):
                        stmt.type = self.extract_meta_info(analyzed, stmt)[0]
                # ...despite possible minor failures that allow further analysis.
                if stmt.type is None or hasattr(stmt, "new_syntax") and not stmt.new_syntax:
                    self.fail(TPDICT_CLASS_ERROR, stmt)
                elif not isinstance(stmt.rvalue, TempNode):
                    # x: int assigns rvalue to TempNode(AnyType())
                    self.fail("Right hand side values are not supported in TypedDict", stmt)
        total: bool | None = True
        if "total" in defn.keywords:
            total = require_bool_literal_argument(self.api, defn.keywords["total"], "total", True)
        if defn.keywords and defn.keywords.keys() != {"total"}:
            for_function = ' for "__init_subclass__" of "TypedDict"'
            for key in defn.keywords:
                if key == "total":
                    continue
                self.msg.unexpected_keyword_argument_for_function(for_function, key, defn)

        res_types = []
        readonly_keys = set()
        required_keys = set()
        for field, t in zip(fields, types):
            typ, required, readonly = self.extract_meta_info(t)
            res_types.append(typ)
            if (total or required is True) and required is not False:
                required_keys.add(field)
            if readonly:
                readonly_keys.add(field)

        return fields, res_types, statements, required_keys, readonly_keys

    def extract_meta_info(
        self, typ: Type, context: Context | None = None
    ) -> tuple[Type, bool | None, bool]:
        """Unwrap all metadata types."""
        is_required = None  # default, no modification
        readonly = False  # by default all is mutable

        seen_required = False
        seen_readonly = False
        while isinstance(typ, (RequiredType, ReadOnlyType)):
            if isinstance(typ, RequiredType):
                if context is not None and seen_required:
                    self.fail(
                        '"{}" type cannot be nested'.format(
                            "Required[]" if typ.required else "NotRequired[]"
                        ),
                        context,
                        code=codes.VALID_TYPE,
                    )
                is_required = typ.required
                seen_required = True
                typ = typ.item
            if isinstance(typ, ReadOnlyType):
                if context is not None and seen_readonly:
                    self.fail('"ReadOnly[]" type cannot be nested', context, code=codes.VALID_TYPE)
                readonly = True
                seen_readonly = True
                typ = typ.item
        return typ, is_required, readonly

    def check_typeddict(
        self, node: Expression, var_name: str | None, is_func_scope: bool
    ) -> tuple[bool, TypeInfo | None, list[TypeVarLikeType]]:
        """Check if a call defines a TypedDict.

        The optional var_name argument is the name of the variable to
        which this is assigned, if any.

        Return a pair (is it a typed dict, corresponding TypeInfo).

        If the definition is invalid but looks like a TypedDict,
        report errors but return (some) TypeInfo. If some type is not ready,
        return (True, None).
        """
        if not isinstance(node, CallExpr):
            return False, None, []
        call = node
        callee = call.callee
        if not isinstance(callee, RefExpr):
            return False, None, []
        fullname = callee.fullname
        if fullname not in TPDICT_NAMES:
            return False, None, []
        res = self.parse_typeddict_args(call)
        if res is None:
            # This is a valid typed dict, but some type is not ready.
            # The caller should defer this until next iteration.
            return True, None, []
        name, items, types, total, tvar_defs, ok = res
        if not ok:
            # Error. Construct dummy return value.
            if var_name:
                name = var_name
                if is_func_scope:
                    name += "@" + str(call.line)
            else:
                name = var_name = "TypedDict@" + str(call.line)
            info = self.build_typeddict_typeinfo(name, [], [], set(), set(), call.line, None)
        else:
            if var_name is not None and name != var_name:
                self.fail(
                    'First argument "{}" to TypedDict() does not match variable name "{}"'.format(
                        name, var_name
                    ),
                    node,
                    code=codes.NAME_MATCH,
                )
            if name != var_name or is_func_scope:
                # Give it a unique name derived from the line number.
                name += "@" + str(call.line)
            required_keys = {
                field
                for (field, t) in zip(items, types)
                if (total or (isinstance(t, RequiredType) and t.required))
                and not (isinstance(t, RequiredType) and not t.required)
            }
            readonly_keys = {
                field for (field, t) in zip(items, types) if isinstance(t, ReadOnlyType)
            }
            types = [  # unwrap Required[T] or ReadOnly[T] to just T
                t.item if isinstance(t, (RequiredType, ReadOnlyType)) else t for t in types
            ]

            # Perform various validations after unwrapping.
            for t in types:
                check_for_explicit_any(
                    t, self.options, self.api.is_typeshed_stub_file, self.msg, context=call
                )
            if self.options.disallow_any_unimported:
                for t in types:
                    if has_any_from_unimported_type(t):
                        self.msg.unimported_type_becomes_any("Type of a TypedDict key", t, call)

            existing_info = None
            if isinstance(node.analyzed, TypedDictExpr):
                existing_info = node.analyzed.info
            info = self.build_typeddict_typeinfo(
                name, items, types, required_keys, readonly_keys, call.line, existing_info
            )
            info.line = node.line
        # Store generated TypeInfo under both names, see semanal_namedtuple for more details.
        if name != var_name or is_func_scope:
            self.api.add_symbol_skip_local(name, info)
        if var_name:
            self.api.add_symbol(var_name, info, node)
        call.analyzed = TypedDictExpr(info)
        call.analyzed.set_line(call)
        return True, info, tvar_defs

    def parse_typeddict_args(
        self, call: CallExpr
    ) -> tuple[str, list[str], list[Type], bool, list[TypeVarLikeType], bool] | None:
        """Parse typed dict call expression.

        Return names, types, totality, was there an error during parsing.
        If some type is not ready, return None.
        """
        # TODO: Share code with check_argument_count in checkexpr.py?
        args = call.args
        if len(args) < 2:
            return self.fail_typeddict_arg("Too few arguments for TypedDict()", call)
        if len(args) > 3:
            return self.fail_typeddict_arg("Too many arguments for TypedDict()", call)
        # TODO: Support keyword arguments
        if call.arg_kinds not in ([ARG_POS, ARG_POS], [ARG_POS, ARG_POS, ARG_NAMED]):
            return self.fail_typeddict_arg("Unexpected arguments to TypedDict()", call)
        if len(args) == 3 and call.arg_names[2] != "total":
            return self.fail_typeddict_arg(
                f'Unexpected keyword argument "{call.arg_names[2]}" for "TypedDict"', call
            )
        if not isinstance(args[0], StrExpr):
            return self.fail_typeddict_arg(
                "TypedDict() expects a string literal as the first argument", call
            )
        if not isinstance(args[1], DictExpr):
            return self.fail_typeddict_arg(
                "TypedDict() expects a dictionary literal as the second argument", call
            )
        total: bool | None = True
        if len(args) == 3:
            total = require_bool_literal_argument(self.api, call.args[2], "total")
            if total is None:
                return "", [], [], True, [], False
        dictexpr = args[1]
        tvar_defs = self.api.get_and_bind_all_tvars([t for k, t in dictexpr.items])
        res = self.parse_typeddict_fields_with_types(dictexpr.items, call)
        if res is None:
            # One of the types is not ready, defer.
            return None
        items, types, ok = res
        assert total is not None
        return args[0].value, items, types, total, tvar_defs, ok

    def parse_typeddict_fields_with_types(
        self, dict_items: list[tuple[Expression | None, Expression]], context: Context
    ) -> tuple[list[str], list[Type], bool] | None:
        """Parse typed dict items passed as pairs (name expression, type expression).

        Return names, types, was there an error. If some type is not ready, return None.
        """
        seen_keys = set()
        items: list[str] = []
        types: list[Type] = []
        for field_name_expr, field_type_expr in dict_items:
            if isinstance(field_name_expr, StrExpr):
                key = field_name_expr.value
                items.append(key)
                if key in seen_keys:
                    self.fail(f'Duplicate TypedDict key "{key}"', field_name_expr)
                seen_keys.add(key)
            else:
                name_context = field_name_expr or field_type_expr
                self.fail_typeddict_arg("Invalid TypedDict() field name", name_context)
                return [], [], False
            try:
                type = expr_to_unanalyzed_type(
                    field_type_expr, self.options, self.api.is_stub_file
                )
            except TypeTranslationError:
                self.fail_typeddict_arg("Use dict literal for nested TypedDict", field_type_expr)
                return [], [], False
            analyzed = self.api.anal_type(
                type,
                allow_typed_dict_special_forms=True,
                allow_placeholder=not self.api.is_func_scope(),
                prohibit_self_type="TypedDict item type",
            )
            if analyzed is None:
                return None
            types.append(analyzed)
        return items, types, True

    def fail_typeddict_arg(
        self, message: str, context: Context
    ) -> tuple[str, list[str], list[Type], bool, list[TypeVarLikeType], bool]:
        self.fail(message, context)
        return "", [], [], True, [], False

    def build_typeddict_typeinfo(
        self,
        name: str,
        items: list[str],
        types: list[Type],
        required_keys: set[str],
        readonly_keys: set[str],
        line: int,
        existing_info: TypeInfo | None,
    ) -> TypeInfo:
        # Prefer typing then typing_extensions if available.
        fallback = (
            self.api.named_type_or_none("typing._TypedDict", [])
            or self.api.named_type_or_none("typing_extensions._TypedDict", [])
            or self.api.named_type_or_none("mypy_extensions._TypedDict", [])
        )
        assert fallback is not None
        info = existing_info or self.api.basic_new_typeinfo(name, fallback, line)
        typeddict_type = TypedDictType(
            dict(zip(items, types)), required_keys, readonly_keys, fallback
        )
        if info.special_alias and has_placeholder(info.special_alias.target):
            self.api.process_placeholder(
                None, "TypedDict item", info, force_progress=typeddict_type != info.typeddict_type
            )
        info.update_typeddict_type(typeddict_type)
        return info

    # Helpers

    def is_typeddict(self, expr: Expression) -> bool:
        return (
            isinstance(expr, RefExpr)
            and isinstance(expr.node, TypeInfo)
            and expr.node.typeddict_type is not None
        )

    def fail(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None:
        self.api.fail(msg, ctx, code=code)

    def note(self, msg: str, ctx: Context) -> None:
        self.api.note(msg, ctx)


"""Utilities for comparing two versions of a module symbol table.

The goal is to find which AST nodes have externally visible changes, so
that we can fire triggers and re-process other parts of the program
that are stale because of the changes.

Only look at detail at definitions at the current module -- don't
recurse into other modules.

A summary of the module contents:

* snapshot_symbol_table(...) creates an opaque snapshot description of a
  module/class symbol table (recursing into nested class symbol tables).

* compare_symbol_table_snapshots(...) compares two snapshots for the same
  module id and returns fully qualified names of differences (which act as
  triggers).

To compare two versions of a module symbol table, take snapshots of both
versions and compare the snapshots. The use of snapshots makes it easy to
compare two versions of the *same* symbol table that is being mutated.

Summary of how this works for certain kinds of differences:

* If a symbol table node is deleted or added (only present in old/new version
  of the symbol table), it is considered different, of course.

* If a symbol table node refers to a different sort of thing in the new version,
  it is considered different (for example, if a class is replaced with a
  function).

* If the signature of a function has changed, it is considered different.

* If the type of a variable changes, it is considered different.

* If the MRO of a class changes, or a non-generic class is turned into a
  generic class, the class is considered different (there are other such "big"
  differences that cause a class to be considered changed). However, just changes
  to attributes or methods don't generally constitute a difference at the
  class level -- these are handled at attribute level (say, 'mod.Cls.method'
  is different rather than 'mod.Cls' being different).

* If an imported name targets a different name (say, 'from x import y' is
  replaced with 'from z import y'), the name in the module is considered
  different. If the target of an import continues to have the same name,
  but it's specifics change, this doesn't mean that the imported name is
  treated as changed. Say, there is 'from x import y' in 'm', and the
  type of 'x.y' has changed. This doesn't mean that that 'm.y' is considered
  changed. Instead, processing the difference in 'm' will be handled through
  fine-grained dependencies.
"""

from __future__ import annotations

from typing import Sequence, Tuple, Union
from typing_extensions import TypeAlias as _TypeAlias

from mypy.expandtype import expand_type
from mypy.nodes import (
    UNBOUND_IMPORTED,
    Decorator,
    FuncBase,
    FuncDef,
    FuncItem,
    MypyFile,
    OverloadedFuncDef,
    ParamSpecExpr,
    SymbolNode,
    SymbolTable,
    TypeAlias,
    TypeInfo,
    TypeVarExpr,
    TypeVarTupleExpr,
    Var,
)
from mypy.semanal_shared import find_dataclass_transform_spec
from mypy.state import state
from mypy.types import (
    AnyType,
    CallableType,
    DeletedType,
    ErasedType,
    Instance,
    LiteralType,
    NoneType,
    Overloaded,
    Parameters,
    ParamSpecType,
    PartialType,
    TupleType,
    Type,
    TypeAliasType,
    TypedDictType,
    TypeType,
    TypeVarId,
    TypeVarLikeType,
    TypeVarTupleType,
    TypeVarType,
    TypeVisitor,
    UnboundType,
    UninhabitedType,
    UnionType,
    UnpackType,
)
from mypy.util import get_prefix

# Snapshot representation of a symbol table node or type. The representation is
# opaque -- the only supported operations are comparing for equality and
# hashing (latter for type snapshots only). Snapshots can contain primitive
# objects, nested tuples, lists and dictionaries and primitive objects (type
# snapshots are immutable).
#
# For example, the snapshot of the 'int' type is ('Instance', 'builtins.int', ()).

# Type snapshots are strict, they must be hashable and ordered (e.g. for Unions).
Primitive: _TypeAlias = Union[str, float, int, bool]  # float is for Literal[3.14] support.
SnapshotItem: _TypeAlias = Tuple[Union[Primitive, "SnapshotItem"], ...]

# Symbol snapshots can be more lenient.
SymbolSnapshot: _TypeAlias = Tuple[object, ...]

def compare_symbol_table_snapshots(
    name_prefix: str, snapshot1: dict[str, SymbolSnapshot], snapshot2: dict[str, SymbolSnapshot]
) -> set[str]:
    """Return names that are different in two snapshots of a symbol table.

    Only shallow (intra-module) differences are considered. References to things defined
    outside the module are compared based on the name of the target only.

    Recurse into class symbol tables (if the class is defined in the target module).

    Return a set of fully-qualified names (e.g., 'mod.func' or 'mod.Class.method').
    """
    # Find names only defined only in one version.
    names1 = {f"{name_prefix}.{name}" for name in snapshot1}
    names2 = {f"{name_prefix}.{name}" for name in snapshot2}
    triggers = names1 ^ names2

    # Look for names defined in both versions that are different.
    for name in set(snapshot1.keys()) & set(snapshot2.keys()):
        item1 = snapshot1[name]
        item2 = snapshot2[name]
        kind1 = item1[0]
        kind2 = item2[0]
        item_name = f"{name_prefix}.{name}"
        if kind1 != kind2:
            # Different kind of node in two snapshots -> trivially different.
            triggers.add(item_name)
        elif kind1 == "TypeInfo":
            if item1[:-1] != item2[:-1]:
                # Record major difference (outside class symbol tables).
                triggers.add(item_name)
            # Look for differences in nested class symbol table entries.
            assert isinstance(item1[-1], dict)
            assert isinstance(item2[-1], dict)
            triggers |= compare_symbol_table_snapshots(item_name, item1[-1], item2[-1])
        else:
            # Shallow node (no interesting internal structure). Just use equality.
            if snapshot1[name] != snapshot2[name]:
                triggers.add(item_name)

    return triggers

def snapshot_symbol_table(name_prefix: str, table: SymbolTable) -> dict[str, SymbolSnapshot]:
    """Create a snapshot description that represents the state of a symbol table.

    The snapshot has a representation based on nested tuples and dicts
    that makes it easy and fast to find differences.

    Only "shallow" state is included in the snapshot -- references to
    things defined in other modules are represented just by the names of
    the targets.
    """
    result: dict[str, SymbolSnapshot] = {}
    for name, symbol in table.items():
        node = symbol.node
        # TODO: cross_ref?
        fullname = node.fullname if node else None
        common = (fullname, symbol.kind, symbol.module_public)
        if isinstance(node, MypyFile):
            # This is a cross-reference to another module.
            # If the reference is busted because the other module is missing,
            # the node will be a "stale_info" TypeInfo produced by fixup,
            # but that doesn't really matter to us here.
            result[name] = ("Moduleref", common)
        elif isinstance(node, TypeVarExpr):
            result[name] = (
                "TypeVar",
                node.variance,
                [snapshot_type(value) for value in node.values],
                snapshot_type(node.upper_bound),
                snapshot_type(node.default),
            )
        elif isinstance(node, TypeAlias):
            result[name] = (
                "TypeAlias",
                snapshot_types(node.alias_tvars),
                node.normalized,
                node.no_args,
                snapshot_optional_type(node.target),
            )
        elif isinstance(node, ParamSpecExpr):
            result[name] = (
                "ParamSpec",
                node.variance,
                snapshot_type(node.upper_bound),
                snapshot_type(node.default),
            )
        elif isinstance(node, TypeVarTupleExpr):
            result[name] = (
                "TypeVarTuple",
                node.variance,
                snapshot_type(node.upper_bound),
                snapshot_type(node.default),
            )
        else:
            assert symbol.kind != UNBOUND_IMPORTED
            if node and get_prefix(node.fullname) != name_prefix:
                # This is a cross-reference to a node defined in another module.
                # Include the node kind (FuncDef, Decorator, TypeInfo, ...), so that we will
                # reprocess when a *new* node is created instead of merging an existing one.
                result[name] = ("CrossRef", common, type(node).__name__)
            else:
                result[name] = snapshot_definition(node, common)
    return result

def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> SymbolSnapshot:
    """Create a snapshot description of a symbol table node.

    The representation is nested tuples and dicts. Only externally
    visible attributes are included.
    """
    if isinstance(node, FuncBase):
        # TODO: info
        if node.type:
            signature = snapshot_type(node.type)
        else:
            signature = snapshot_untyped_signature(node)
        impl: FuncDef | None = None
        if isinstance(node, FuncDef):
            impl = node
        elif isinstance(node, OverloadedFuncDef) and node.impl:
            impl = node.impl.func if isinstance(node.impl, Decorator) else node.impl
        is_trivial_body = impl.is_trivial_body if impl else False
        dataclass_transform_spec = find_dataclass_transform_spec(node)
        return (
            "Func",
            common,
            node.is_property,
            node.is_final,
            node.is_class,
            node.is_static,
            signature,
            is_trivial_body,
            dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None,
        )
    elif isinstance(node, Var):
        return ("Var", common, snapshot_optional_type(node.type), node.is_final)
    elif isinstance(node, Decorator):
        # Note that decorated methods are represented by Decorator instances in
        # a symbol table since we need to preserve information about the
        # decorated function (whether it's a class function, for
        # example). Top-level decorated functions, however, are represented by
        # the corresponding Var node, since that happens to provide enough
        # context.
        return (
            "Decorator",
            node.is_overload,
            snapshot_optional_type(node.var.type),
            snapshot_definition(node.func, common),
        )
    elif isinstance(node, TypeInfo):
        dataclass_transform_spec = node.dataclass_transform_spec
        if dataclass_transform_spec is None:
            dataclass_transform_spec = find_dataclass_transform_spec(node)

        attrs = (
            node.is_abstract,
            node.is_enum,
            node.is_protocol,
            node.fallback_to_any,
            node.meta_fallback_to_any,
            node.is_named_tuple,
            node.is_newtype,
            # We need this to e.g. trigger metaclass calculation in subclasses.
            snapshot_optional_type(node.metaclass_type),
            snapshot_optional_type(node.tuple_type),
            snapshot_optional_type(node.typeddict_type),
            [base.fullname for base in node.mro],
            # Note that the structure of type variables is a part of the external interface,
            # since creating instances might fail, for example:
            #     T = TypeVar('T', bound=int)
            #     class C(Generic[T]):
            #         ...
            #     x: C[str] <- this is invalid, and needs to be re-checked if `T` changes.
            # An alternative would be to create both deps: <...> -> C, and <...> -> <C>,
            # but this currently seems a bit ad hoc.
            tuple(snapshot_type(tdef) for tdef in node.defn.type_vars),
            [snapshot_type(base) for base in node.bases],
            [snapshot_type(p) for p in node._promote],
            dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None,
        )
        prefix = node.fullname
        symbol_table = snapshot_symbol_table(prefix, node.names)
        # Special dependency for abstract attribute handling.
        symbol_table["(abstract)"] = ("Abstract", tuple(sorted(node.abstract_attributes)))
        return ("TypeInfo", common, attrs, symbol_table)
    else:
        # Other node types are handled elsewhere.
        assert False, type(node)

def snapshot_type(typ: Type) -> SnapshotItem:
    """Create a snapshot representation of a type using nested tuples."""
    return typ.accept(SnapshotTypeVisitor())

def snapshot_optional_type(typ: Type | None) -> SnapshotItem:
    if typ:
        return snapshot_type(typ)
    else:
        return ("<not set>",)

def snapshot_types(types: Sequence[Type]) -> SnapshotItem:
    return tuple(snapshot_type(item) for item in types)

def snapshot_simple_type(typ: Type) -> SnapshotItem:
    return (type(typ).__name__,)

def encode_optional_str(s: str | None) -> str:
    if s is None:
        return "<None>"
    else:
        return s

class SnapshotTypeVisitor(TypeVisitor[SnapshotItem]):
    """Creates a read-only, self-contained snapshot of a type object.

    Properties of a snapshot:

    - Contains (nested) tuples and other immutable primitive objects only.
    - References to AST nodes are replaced with full names of targets.
    - Has no references to mutable or non-primitive objects.
    - Two snapshots represent the same object if and only if they are
      equal.
    - Results must be sortable. It's important that tuples have
      consistent types and can't arbitrarily mix str and None values,
      for example, since they can't be compared.
    """

    def visit_unbound_type(self, typ: UnboundType) -> SnapshotItem:
        return (
            "UnboundType",
            typ.name,
            typ.optional,
            typ.empty_tuple_index,
            snapshot_types(typ.args),
        )

    def visit_any(self, typ: AnyType) -> SnapshotItem:
        return snapshot_simple_type(typ)

    def visit_none_type(self, typ: NoneType) -> SnapshotItem:
        return snapshot_simple_type(typ)

    def visit_uninhabited_type(self, typ: UninhabitedType) -> SnapshotItem:
        return snapshot_simple_type(typ)

    def visit_erased_type(self, typ: ErasedType) -> SnapshotItem:
        return snapshot_simple_type(typ)

    def visit_deleted_type(self, typ: DeletedType) -> SnapshotItem:
        return snapshot_simple_type(typ)

    def visit_instance(self, typ: Instance) -> SnapshotItem:
        extra_attrs: SnapshotItem
        if typ.extra_attrs:
            extra_attrs = (
                tuple(sorted((k, v.accept(self)) for k, v in typ.extra_attrs.attrs.items())),
                tuple(typ.extra_attrs.immutable),
            )
        else:
            extra_attrs = ()
        return (
            "Instance",
            encode_optional_str(typ.type.fullname),
            snapshot_types(typ.args),
            ("None",) if typ.last_known_value is None else snapshot_type(typ.last_known_value),
            extra_attrs,
        )

    def visit_type_var(self, typ: TypeVarType) -> SnapshotItem:
        return (
            "TypeVar",
            typ.name,
            typ.fullname,
            typ.id.raw_id,
            typ.id.meta_level,
            snapshot_types(typ.values),
            snapshot_type(typ.upper_bound),
            snapshot_type(typ.default),
            typ.variance,
        )

    def visit_param_spec(self, typ: ParamSpecType) -> SnapshotItem:
        return (
            "ParamSpec",
            typ.id.raw_id,
            typ.id.meta_level,
            typ.flavor,
            snapshot_type(typ.upper_bound),
            snapshot_type(typ.default),
        )

    def visit_type_var_tuple(self, typ: TypeVarTupleType) -> SnapshotItem:
        return (
            "TypeVarTupleType",
            typ.id.raw_id,
            typ.id.meta_level,
            snapshot_type(typ.upper_bound),
            snapshot_type(typ.default),
        )

    def visit_unpack_type(self, typ: UnpackType) -> SnapshotItem:
        return ("UnpackType", snapshot_type(typ.type))

    def visit_parameters(self, typ: Parameters) -> SnapshotItem:
        return (
            "Parameters",
            snapshot_types(typ.arg_types),
            tuple(encode_optional_str(name) for name in typ.arg_names),
            tuple(k.value for k in typ.arg_kinds),
        )

    def visit_callable_type(self, typ: CallableType) -> SnapshotItem:
        if typ.is_generic():
            typ = self.normalize_callable_variables(typ)
        return (
            "CallableType",
            snapshot_types(typ.arg_types),
            snapshot_type(typ.ret_type),
            tuple(encode_optional_str(name) for name in typ.arg_names),
            tuple(k.value for k in typ.arg_kinds),
            typ.is_type_obj(),
            typ.is_ellipsis_args,
            snapshot_types(typ.variables),
        )

    def normalize_callable_variables(self, typ: CallableType) -> CallableType:
        """Normalize all type variable ids to run from -1 to -len(variables)."""
        tvs = []
        tvmap: dict[TypeVarId, Type] = {}
        for i, v in enumerate(typ.variables):
            tid = TypeVarId(-1 - i)
            if isinstance(v, TypeVarType):
                tv: TypeVarLikeType = v.copy_modified(id=tid)
            elif isinstance(v, TypeVarTupleType):
                tv = v.copy_modified(id=tid)
            else:
                assert isinstance(v, ParamSpecType)
                tv = v.copy_modified(id=tid)
            tvs.append(tv)
            tvmap[v.id] = tv
        with state.strict_optional_set(True):
            return expand_type(typ, tvmap).copy_modified(variables=tvs)

    def visit_tuple_type(self, typ: TupleType) -> SnapshotItem:
        return ("TupleType", snapshot_types(typ.items))

    def visit_typeddict_type(self, typ: TypedDictType) -> SnapshotItem:
        items = tuple((key, snapshot_type(item_type)) for key, item_type in typ.items.items())
        required = tuple(sorted(typ.required_keys))
        readonly = tuple(sorted(typ.readonly_keys))
        return ("TypedDictType", items, required, readonly)

    def visit_literal_type(self, typ: LiteralType) -> SnapshotItem:
        return ("LiteralType", snapshot_type(typ.fallback), typ.value)

    def visit_union_type(self, typ: UnionType) -> SnapshotItem:
        # Sort and remove duplicates so that we can use equality to test for
        # equivalent union type snapshots.
        items = {snapshot_type(item) for item in typ.items}
        normalized = tuple(sorted(items))
        return ("UnionType", normalized)

    def visit_overloaded(self, typ: Overloaded) -> SnapshotItem:
        return ("Overloaded", snapshot_types(typ.items))

    def visit_partial_type(self, typ: PartialType) -> SnapshotItem:
        # A partial type is not fully defined, so the result is indeterminate. We shouldn't
        # get here.
        raise RuntimeError

    def visit_type_type(self, typ: TypeType) -> SnapshotItem:
        return ("TypeType", snapshot_type(typ.item))

    def visit_type_alias_type(self, typ: TypeAliasType) -> SnapshotItem:
        assert typ.alias is not None
        return ("TypeAliasType", typ.alias.fullname, snapshot_types(typ.args))

def snapshot_untyped_signature(func: OverloadedFuncDef | FuncItem) -> SymbolSnapshot:
    """Create a snapshot of the signature of a function that has no explicit signature.

    If the arguments to a function without signature change, it must be
    considered as different. We have this special casing since we don't store
    the implicit signature anywhere, and we'd rather not construct new
    Callable objects in this module (the idea is to only read properties of
    the AST here).
    """
    if isinstance(func, FuncItem):
        return (tuple(func.arg_names), tuple(func.arg_kinds))
    else:
        result: list[SymbolSnapshot] = []
        for item in func.items:
            if isinstance(item, Decorator):
                if item.var.type:
                    result.append(snapshot_type(item.var.type))
                else:
                    result.append(("DecoratorWithoutType",))
            else:
                result.append(snapshot_untyped_signature(item))
        return tuple(result)


"""Merge a new version of a module AST and symbol table to older versions of those.

When the source code of a module has a change in fine-grained incremental mode,
we build a new AST from the updated source. However, other parts of the program
may have direct references to parts of the old AST (namely, those nodes exposed
in the module symbol table). The merge operation changes the identities of new
AST nodes that have a correspondence in the old AST to the old ones so that
existing cross-references in other modules will continue to point to the correct
nodes. Also internal cross-references within the new AST are replaced. AST nodes
that aren't externally visible will get new, distinct object identities. This
applies to most expression and statement nodes, for example.

We perform this merge operation so that we don't have to update all
external references (which would be slow and fragile) or always perform
translation when looking up references (which would be hard to retrofit).

The AST merge operation is performed after semantic analysis. Semantic
analysis has to deal with potentially multiple aliases to certain AST
nodes (in particular, MypyFile nodes). Type checking assumes that we
don't have multiple variants of a single AST node visible to the type
checker.

Discussion of some notable special cases:

* If a node is replaced with a different kind of node (say, a function is
  replaced with a class), we don't perform the merge. Fine-grained dependencies
  will be used to rebind all references to the node.

* If a function is replaced with another function with an identical signature,
  call sites continue to point to the same object (by identity) and don't need
  to be reprocessed. Similarly, if a class is replaced with a class that is
  sufficiently similar (MRO preserved, etc.), class references don't need any
  processing. A typical incremental update to a file only changes a few
  externally visible things in a module, and this means that often only few
  external references need any processing, even if the modified module is large.

* A no-op update of a module should not require any processing outside the
  module, since all relevant object identities are preserved.

* The AST diff operation (mypy.server.astdiff) and the top-level fine-grained
  incremental logic (mypy.server.update) handle the cases where the new AST has
  differences from the old one that may need to be propagated to elsewhere in the
  program.

See the main entry point merge_asts for more details.
"""

from __future__ import annotations

from typing import TypeVar, cast

from mypy.nodes import (
    MDEF,
    AssertTypeExpr,
    AssignmentStmt,
    Block,
    CallExpr,
    CastExpr,
    ClassDef,
    EnumCallExpr,
    FuncBase,
    FuncDef,
    LambdaExpr,
    MemberExpr,
    MypyFile,
    NamedTupleExpr,
    NameExpr,
    NewTypeExpr,
    OverloadedFuncDef,
    RefExpr,
    Statement,
    SuperExpr,
    SymbolNode,
    SymbolTable,
    TypeAlias,
    TypedDictExpr,
    TypeInfo,
    Var,
)
from mypy.traverser import TraverserVisitor
from mypy.types import (
    AnyType,
    CallableArgument,
    CallableType,
    DeletedType,
    EllipsisType,
    ErasedType,
    Instance,
    LiteralType,
    NoneType,
    Overloaded,
    Parameters,
    ParamSpecType,
    PartialType,
    PlaceholderType,
    RawExpressionType,
    SyntheticTypeVisitor,
    TupleType,
    Type,
    TypeAliasType,
    TypedDictType,
    TypeList,
    TypeType,
    TypeVarTupleType,
    TypeVarType,
    UnboundType,
    UninhabitedType,
    UnionType,
    UnpackType,
)
from mypy.typestate import type_state
from mypy.util import get_prefix, replace_object_state

def merge_asts(
    old: MypyFile, old_symbols: SymbolTable, new: MypyFile, new_symbols: SymbolTable
) -> None:
    """Merge a new version of a module AST to a previous version.

    The main idea is to preserve the identities of externally visible
    nodes in the old AST (that have a corresponding node in the new AST).
    All old node state (outside identity) will come from the new AST.

    When this returns, 'old' will refer to the merged AST, but 'new_symbols'
    will be the new symbol table. 'new' and 'old_symbols' will no longer be
    valid.
    """
    assert new.fullname == old.fullname
    # Find the mapping from new to old node identities for all nodes
    # whose identities should be preserved.
    replacement_map = replacement_map_from_symbol_table(
        old_symbols, new_symbols, prefix=old.fullname
    )
    # Also replace references to the new MypyFile node.
    replacement_map[new] = old
    # Perform replacements to everywhere within the new AST (not including symbol
    # tables).
    node = replace_nodes_in_ast(new, replacement_map)
    assert node is old
    # Also replace AST node references in the *new* symbol table (we'll
    # continue to use the new symbol table since it has all the new definitions
    # that have no correspondence in the old AST).
    replace_nodes_in_symbol_table(new_symbols, replacement_map)

def replacement_map_from_symbol_table(
    old: SymbolTable, new: SymbolTable, prefix: str
) -> dict[SymbolNode, SymbolNode]:
    """Create a new-to-old object identity map by comparing two symbol table revisions.

    Both symbol tables must refer to revisions of the same module id. The symbol tables
    are compared recursively (recursing into nested class symbol tables), but only within
    the given module prefix. Don't recurse into other modules accessible through the symbol
    table.
    """
    replacements: dict[SymbolNode, SymbolNode] = {}
    for name, node in old.items():
        if name in new and (
            node.kind == MDEF or node.node and get_prefix(node.node.fullname) == prefix
        ):
            new_node = new[name]
            if (
                type(new_node.node) == type(node.node)  # noqa: E721
                and new_node.node
                and node.node
                and new_node.node.fullname == node.node.fullname
                and new_node.kind == node.kind
            ):
                replacements[new_node.node] = node.node
                if isinstance(node.node, TypeInfo) and isinstance(new_node.node, TypeInfo):
                    type_repl = replacement_map_from_symbol_table(
                        node.node.names, new_node.node.names, prefix
                    )
                    replacements.update(type_repl)
                    if node.node.special_alias and new_node.node.special_alias:
                        replacements[new_node.node.special_alias] = node.node.special_alias
    return replacements

def replace_nodes_in_ast(
    node: SymbolNode, replacements: dict[SymbolNode, SymbolNode]
) -> SymbolNode:
    """Replace all references to replacement map keys within an AST node, recursively.

    Also replace the *identity* of any nodes that have replacements. Return the
    *replaced* version of the argument node (which may have a different identity, if
    it's included in the replacement map).
    """
    visitor = NodeReplaceVisitor(replacements)
    node.accept(visitor)
    return replacements.get(node, node)

SN = TypeVar("SN", bound=SymbolNode)

class NodeReplaceVisitor(TraverserVisitor):
    """Transform some nodes to new identities in an AST.

    Only nodes that live in the symbol table may be
    replaced, which simplifies the implementation some. Also
    replace all references to the old identities.
    """

    def __init__(self, replacements: dict[SymbolNode, SymbolNode]) -> None:
        self.replacements = replacements

    def visit_mypy_file(self, node: MypyFile) -> None:
        node = self.fixup(node)
        node.defs = self.replace_statements(node.defs)
        super().visit_mypy_file(node)

    def visit_block(self, node: Block) -> None:
        node.body = self.replace_statements(node.body)
        super().visit_block(node)

    def visit_func_def(self, node: FuncDef) -> None:
        node = self.fixup(node)
        self.process_base_func(node)
        super().visit_func_def(node)

    def visit_overloaded_func_def(self, node: OverloadedFuncDef) -> None:
        self.process_base_func(node)
        super().visit_overloaded_func_def(node)

    def visit_class_def(self, node: ClassDef) -> None:
        # TODO additional things?
        node.info = self.fixup_and_reset_typeinfo(node.info)
        node.defs.body = self.replace_statements(node.defs.body)
        info = node.info
        for tv in node.type_vars:
            if isinstance(tv, TypeVarType):
                self.process_type_var_def(tv)
        if info:
            if info.is_named_tuple:
                self.process_synthetic_type_info(info)
            else:
                self.process_type_info(info)
        super().visit_class_def(node)

    def process_base_func(self, node: FuncBase) -> None:
        self.fixup_type(node.type)
        node.info = self.fixup(node.info)
        if node.unanalyzed_type:
            # Unanalyzed types can have AST node references
            self.fixup_type(node.unanalyzed_type)

    def process_type_var_def(self, tv: TypeVarType) -> None:
        for value in tv.values:
            self.fixup_type(value)
        self.fixup_type(tv.upper_bound)
        self.fixup_type(tv.default)

    def process_param_spec_def(self, tv: ParamSpecType) -> None:
        self.fixup_type(tv.upper_bound)
        self.fixup_type(tv.default)

    def process_type_var_tuple_def(self, tv: TypeVarTupleType) -> None:
        self.fixup_type(tv.upper_bound)
        self.fixup_type(tv.default)

    def visit_assignment_stmt(self, node: AssignmentStmt) -> None:
        self.fixup_type(node.type)
        super().visit_assignment_stmt(node)

    # Expressions

    def visit_name_expr(self, node: NameExpr) -> None:
        self.visit_ref_expr(node)

    def visit_member_expr(self, node: MemberExpr) -> None:
        if node.def_var:
            node.def_var = self.fixup(node.def_var)
        self.visit_ref_expr(node)
        super().visit_member_expr(node)

    def visit_ref_expr(self, node: RefExpr) -> None:
        if node.node is not None:
            node.node = self.fixup(node.node)
            if isinstance(node.node, Var):
                # The Var node may be an orphan and won't otherwise be processed.
                node.node.accept(self)

    def visit_namedtuple_expr(self, node: NamedTupleExpr) -> None:
        super().visit_namedtuple_expr(node)
        node.info = self.fixup_and_reset_typeinfo(node.info)
        self.process_synthetic_type_info(node.info)

    def visit_cast_expr(self, node: CastExpr) -> None:
        super().visit_cast_expr(node)
        self.fixup_type(node.type)

    def visit_assert_type_expr(self, node: AssertTypeExpr) -> None:
        super().visit_assert_type_expr(node)
        self.fixup_type(node.type)

    def visit_super_expr(self, node: SuperExpr) -> None:
        super().visit_super_expr(node)
        if node.info is not None:
            node.info = self.fixup(node.info)

    def visit_call_expr(self, node: CallExpr) -> None:
        super().visit_call_expr(node)
        if isinstance(node.analyzed, SymbolNode):
            node.analyzed = self.fixup(node.analyzed)

    def visit_newtype_expr(self, node: NewTypeExpr) -> None:
        if node.info:
            node.info = self.fixup_and_reset_typeinfo(node.info)
            self.process_synthetic_type_info(node.info)
        self.fixup_type(node.old_type)
        super().visit_newtype_expr(node)

    def visit_lambda_expr(self, node: LambdaExpr) -> None:
        node.info = self.fixup(node.info)
        super().visit_lambda_expr(node)

    def visit_typeddict_expr(self, node: TypedDictExpr) -> None:
        super().visit_typeddict_expr(node)
        node.info = self.fixup_and_reset_typeinfo(node.info)
        self.process_synthetic_type_info(node.info)

    def visit_enum_call_expr(self, node: EnumCallExpr) -> None:
        node.info = self.fixup_and_reset_typeinfo(node.info)
        self.process_synthetic_type_info(node.info)
        super().visit_enum_call_expr(node)

    # Others

    def visit_var(self, node: Var) -> None:
        node.info = self.fixup(node.info)
        self.fixup_type(node.type)
        super().visit_var(node)

    def visit_type_alias(self, node: TypeAlias) -> None:
        self.fixup_type(node.target)
        for v in node.alias_tvars:
            self.fixup_type(v)
        super().visit_type_alias(node)

    # Helpers

    def fixup(self, node: SN) -> SN:
        if node in self.replacements:
            new = self.replacements[node]
            skip_slots: tuple[str, ...] = ()
            if isinstance(node, TypeInfo) and isinstance(new, TypeInfo):
                # Special case: special_alias is not exposed in symbol tables, but may appear
                # in external types (e.g. named tuples), so we need to update it manually.
                skip_slots = ("special_alias",)
                replace_object_state(new.special_alias, node.special_alias)
            replace_object_state(new, node, skip_slots=skip_slots)
            return cast(SN, new)
        return node

    def fixup_and_reset_typeinfo(self, node: TypeInfo) -> TypeInfo:
        """Fix-up type info and reset subtype caches.

        This needs to be called at least once per each merged TypeInfo, as otherwise we
        may leak stale caches.
        """
        if node in self.replacements:
            # The subclass relationships may change, so reset all caches relevant to the
            # old MRO.
            new = self.replacements[node]
            assert isinstance(new, TypeInfo)
            type_state.reset_all_subtype_caches_for(new)
        return self.fixup(node)

    def fixup_type(self, typ: Type | None) -> None:
        if typ is not None:
            typ.accept(TypeReplaceVisitor(self.replacements))

    def process_type_info(self, info: TypeInfo | None) -> None:
        if info is None:
            return
        self.fixup_type(info.declared_metaclass)
        self.fixup_type(info.metaclass_type)
        for target in info._promote:
            self.fixup_type(target)
        self.fixup_type(info.tuple_type)
        self.fixup_type(info.typeddict_type)
        if info.special_alias:
            self.fixup_type(info.special_alias.target)
        info.defn.info = self.fixup(info)
        replace_nodes_in_symbol_table(info.names, self.replacements)
        for i, item in enumerate(info.mro):
            info.mro[i] = self.fixup(info.mro[i])
        for i, base in enumerate(info.bases):
            self.fixup_type(info.bases[i])

    def process_synthetic_type_info(self, info: TypeInfo) -> None:
        # Synthetic types (types not created using a class statement) don't
        # have bodies in the AST so we need to iterate over their symbol
        # tables separately, unlike normal classes.
        self.process_type_info(info)
        for node in info.names.values():
            if node.node:
                node.node.accept(self)

    def replace_statements(self, nodes: list[Statement]) -> list[Statement]:
        result = []
        for node in nodes:
            if isinstance(node, SymbolNode):
                node = self.fixup(node)
            result.append(node)
        return result

class TypeReplaceVisitor(SyntheticTypeVisitor[None]):
    """Similar to NodeReplaceVisitor, but for type objects.

    Note: this visitor may sometimes visit unanalyzed types
    such as 'UnboundType' and 'RawExpressionType' For example, see
    NodeReplaceVisitor.process_base_func.
    """

    def __init__(self, replacements: dict[SymbolNode, SymbolNode]) -> None:
        self.replacements = replacements

    def visit_instance(self, typ: Instance) -> None:
        typ.type = self.fixup(typ.type)
        for arg in typ.args:
            arg.accept(self)
        if typ.last_known_value:
            typ.last_known_value.accept(self)

    def visit_type_alias_type(self, typ: TypeAliasType) -> None:
        assert typ.alias is not None
        typ.alias = self.fixup(typ.alias)
        for arg in typ.args:
            arg.accept(self)

    def visit_any(self, typ: AnyType) -> None:
        pass

    def visit_none_type(self, typ: NoneType) -> None:
        pass

    def visit_callable_type(self, typ: CallableType) -> None:
        for arg in typ.arg_types:
            arg.accept(self)
        typ.ret_type.accept(self)
        if typ.definition:
            # No need to fixup since this is just a cross-reference.
            typ.definition = self.replacements.get(typ.definition, typ.definition)
        # Fallback can be None for callable types that haven't been semantically analyzed.
        if typ.fallback is not None:
            typ.fallback.accept(self)
        for tv in typ.variables:
            if isinstance(tv, TypeVarType):
                tv.upper_bound.accept(self)
                for value in tv.values:
                    value.accept(self)

    def visit_overloaded(self, t: Overloaded) -> None:
        for item in t.items:
            item.accept(self)
        # Fallback can be None for overloaded types that haven't been semantically analyzed.
        if t.fallback is not None:
            t.fallback.accept(self)

    def visit_erased_type(self, t: ErasedType) -> None:
        # This type should exist only temporarily during type inference
        raise RuntimeError("Cannot handle erased type")

    def visit_deleted_type(self, typ: DeletedType) -> None:
        pass

    def visit_partial_type(self, typ: PartialType) -> None:
        raise RuntimeError("Cannot handle partial type")

    def visit_tuple_type(self, typ: TupleType) -> None:
        for item in typ.items:
            item.accept(self)
        # Fallback can be None for implicit tuple types that haven't been semantically analyzed.
        if typ.partial_fallback is not None:
            typ.partial_fallback.accept(self)

    def visit_type_type(self, typ: TypeType) -> None:
        typ.item.accept(self)

    def visit_type_var(self, typ: TypeVarType) -> None:
        typ.upper_bound.accept(self)
        typ.default.accept(self)
        for value in typ.values:
            value.accept(self)

    def visit_param_spec(self, typ: ParamSpecType) -> None:
        typ.upper_bound.accept(self)
        typ.default.accept(self)

    def visit_type_var_tuple(self, typ: TypeVarTupleType) -> None:
        typ.upper_bound.accept(self)
        typ.default.accept(self)

    def visit_unpack_type(self, typ: UnpackType) -> None:
        typ.type.accept(self)

    def visit_parameters(self, typ: Parameters) -> None:
        for arg in typ.arg_types:
            arg.accept(self)

    def visit_typeddict_type(self, typ: TypedDictType) -> None:
        for value_type in typ.items.values():
            value_type.accept(self)
        typ.fallback.accept(self)

    def visit_raw_expression_type(self, t: RawExpressionType) -> None:
        pass

    def visit_literal_type(self, typ: LiteralType) -> None:
        typ.fallback.accept(self)

    def visit_unbound_type(self, typ: UnboundType) -> None:
        for arg in typ.args:
            arg.accept(self)

    def visit_type_list(self, typ: TypeList) -> None:
        for item in typ.items:
            item.accept(self)

    def visit_callable_argument(self, typ: CallableArgument) -> None:
        typ.typ.accept(self)

    def visit_ellipsis_type(self, typ: EllipsisType) -> None:
        pass

    def visit_uninhabited_type(self, typ: UninhabitedType) -> None:
        pass

    def visit_union_type(self, typ: UnionType) -> None:
        for item in typ.items:
            item.accept(self)

    def visit_placeholder_type(self, t: PlaceholderType) -> None:
        for item in t.args:
            item.accept(self)

    # Helpers

    def fixup(self, node: SN) -> SN:
        if node in self.replacements:
            new = self.replacements[node]
            return cast(SN, new)
        return node

def replace_nodes_in_symbol_table(
    symbols: SymbolTable, replacements: dict[SymbolNode, SymbolNode]
) -> None:
    for node in symbols.values():
        if node.node:
            if node.node in replacements:
                new = replacements[node.node]
                old = node.node
                # Needed for TypeInfo, see comment in fixup() above.
                replace_object_state(new, old, skip_slots=("special_alias",))
                node.node = new
            if isinstance(node.node, (Var, TypeAlias)):
                # Handle them here just in case these aren't exposed through the AST.
                node.node.accept(NodeReplaceVisitor(replacements))