"""Fix up various things after deserialization."""

from __future__ import annotations

from typing import Any, Final

from mypy.lookup import lookup_fully_qualified
from mypy.nodes import (
    Block,
    ClassDef,
    Decorator,
    FuncDef,
    MypyFile,
    OverloadedFuncDef,
    ParamSpecExpr,
    SymbolTable,
    TypeAlias,
    TypeInfo,
    TypeVarExpr,
    TypeVarTupleExpr,
    Var,
)
from mypy.types import (
    NOT_READY,
    AnyType,
    CallableType,
    Instance,
    LiteralType,
    Overloaded,
    Parameters,
    ParamSpecType,
    TupleType,
    TypeAliasType,
    TypedDictType,
    TypeOfAny,
    TypeType,
    TypeVarTupleType,
    TypeVarType,
    TypeVisitor,
    UnboundType,
    UnionType,
    UnpackType,
)
from mypy.visitor import NodeVisitor

# N.B: we do a allow_missing fixup when fixing up a fine-grained
# incremental cache load (since there may be cross-refs into deleted
# modules)
def fixup_module(tree: MypyFile, modules: dict[str, MypyFile], allow_missing: bool) -> None:
    node_fixer = NodeFixer(modules, allow_missing)
    node_fixer.visit_symbol_table(tree.names, tree.fullname)

# TODO: Fix up .info when deserializing, i.e. much earlier.
class NodeFixer(NodeVisitor[None]):
    current_info: TypeInfo | None = None

    def __init__(self, modules: dict[str, MypyFile], allow_missing: bool) -> None:
        self.modules = modules
        self.allow_missing = allow_missing
        self.type_fixer = TypeFixer(self.modules, allow_missing)

    # NOTE: This method isn't (yet) part of the NodeVisitor API.
    def visit_type_info(self, info: TypeInfo) -> None:
        save_info = self.current_info
        try:
            self.current_info = info
            if info.defn:
                info.defn.accept(self)
            if info.names:
                self.visit_symbol_table(info.names, info.fullname)
            if info.bases:
                for base in info.bases:
                    base.accept(self.type_fixer)
            if info._promote:
                for p in info._promote:
                    p.accept(self.type_fixer)
            if info.tuple_type:
                info.tuple_type.accept(self.type_fixer)
                info.update_tuple_type(info.tuple_type)
                if info.special_alias:
                    info.special_alias.alias_tvars = list(info.defn.type_vars)
                    for i, t in enumerate(info.defn.type_vars):
                        if isinstance(t, TypeVarTupleType):
                            info.special_alias.tvar_tuple_index = i
            if info.typeddict_type:
                info.typeddict_type.accept(self.type_fixer)
                info.update_typeddict_type(info.typeddict_type)
                if info.special_alias:
                    info.special_alias.alias_tvars = list(info.defn.type_vars)
                    for i, t in enumerate(info.defn.type_vars):
                        if isinstance(t, TypeVarTupleType):
                            info.special_alias.tvar_tuple_index = i
            if info.declared_metaclass:
                info.declared_metaclass.accept(self.type_fixer)
            if info.metaclass_type:
                info.metaclass_type.accept(self.type_fixer)
            if info.alt_promote:
                info.alt_promote.accept(self.type_fixer)
                instance = Instance(info, [])
                # Hack: We may also need to add a backwards promotion (from int to native int),
                # since it might not be serialized.
                if instance not in info.alt_promote.type._promote:
                    info.alt_promote.type._promote.append(instance)
            if info._mro_refs:
                info.mro = [
                    lookup_fully_qualified_typeinfo(
                        self.modules, name, allow_missing=self.allow_missing
                    )
                    for name in info._mro_refs
                ]
                info._mro_refs = None
        finally:
            self.current_info = save_info

    # NOTE: This method *definitely* isn't part of the NodeVisitor API.
    def visit_symbol_table(self, symtab: SymbolTable, table_fullname: str) -> None:
        # Copy the items because we may mutate symtab.
        for key, value in list(symtab.items()):
            cross_ref = value.cross_ref
            if cross_ref is not None:  # Fix up cross-reference.
                value.cross_ref = None
                if cross_ref in self.modules:
                    value.node = self.modules[cross_ref]
                else:
                    stnode = lookup_fully_qualified(
                        cross_ref, self.modules, raise_on_missing=not self.allow_missing
                    )
                    if stnode is not None:
                        if stnode is value:
                            # The node seems to refer to itself, which can mean that
                            # the target is a deleted submodule of the current module,
                            # and thus lookup falls back to the symbol table of the parent
                            # package. Here's how this may happen:
                            #
                            #   pkg/__init__.py:
                            #     from pkg import sub
                            #
                            # Now if pkg.sub is deleted, the pkg.sub symbol table entry
                            # appears to refer to itself. Replace the entry with a
                            # placeholder to avoid a crash. We can't delete the entry,
                            # as it would stop dependency propagation.
                            value.node = Var(key + "@deleted")
                        else:
                            assert stnode.node is not None, (table_fullname + "." + key, cross_ref)
                            value.node = stnode.node
                    elif not self.allow_missing:
                        assert False, f"Could not find cross-ref {cross_ref}"
                    else:
                        # We have a missing crossref in allow missing mode, need to put something
                        value.node = missing_info(self.modules)
            else:
                if isinstance(value.node, TypeInfo):
                    # TypeInfo has no accept().  TODO: Add it?
                    self.visit_type_info(value.node)
                elif value.node is not None:
                    value.node.accept(self)
                else:
                    assert False, f"Unexpected empty node {key!r}: {value}"

    def visit_func_def(self, func: FuncDef) -> None:
        if self.current_info is not None:
            func.info = self.current_info
        if func.type is not None:
            func.type.accept(self.type_fixer)

    def visit_overloaded_func_def(self, o: OverloadedFuncDef) -> None:
        if self.current_info is not None:
            o.info = self.current_info
        if o.type:
            o.type.accept(self.type_fixer)
        for item in o.items:
            item.accept(self)
        if o.impl:
            o.impl.accept(self)

    def visit_decorator(self, d: Decorator) -> None:
        if self.current_info is not None:
            d.var.info = self.current_info
        if d.func:
            d.func.accept(self)
        if d.var:
            d.var.accept(self)
        for node in d.decorators:
            node.accept(self)

    def visit_class_def(self, c: ClassDef) -> None:
        for v in c.type_vars:
            v.accept(self.type_fixer)

    def visit_type_var_expr(self, tv: TypeVarExpr) -> None:
        for value in tv.values:
            value.accept(self.type_fixer)
        tv.upper_bound.accept(self.type_fixer)
        tv.default.accept(self.type_fixer)

    def visit_paramspec_expr(self, p: ParamSpecExpr) -> None:
        p.upper_bound.accept(self.type_fixer)
        p.default.accept(self.type_fixer)

    def visit_type_var_tuple_expr(self, tv: TypeVarTupleExpr) -> None:
        tv.upper_bound.accept(self.type_fixer)
        tv.tuple_fallback.accept(self.type_fixer)
        tv.default.accept(self.type_fixer)

    def visit_var(self, v: Var) -> None:
        if self.current_info is not None:
            v.info = self.current_info
        if v.type is not None:
            v.type.accept(self.type_fixer)

    def visit_type_alias(self, a: TypeAlias) -> None:
        a.target.accept(self.type_fixer)
        for v in a.alias_tvars:
            v.accept(self.type_fixer)

class TypeFixer(TypeVisitor[None]):
    def __init__(self, modules: dict[str, MypyFile], allow_missing: bool) -> None:
        self.modules = modules
        self.allow_missing = allow_missing

    def visit_instance(self, inst: Instance) -> None:
        # TODO: Combine Instances that are exactly the same?
        type_ref = inst.type_ref
        if type_ref is None:
            return  # We've already been here.
        inst.type_ref = None
        inst.type = lookup_fully_qualified_typeinfo(
            self.modules, type_ref, allow_missing=self.allow_missing
        )
        # TODO: Is this needed or redundant?
        # Also fix up the bases, just in case.
        for base in inst.type.bases:
            if base.type is NOT_READY:
                base.accept(self)
        for a in inst.args:
            a.accept(self)
        if inst.last_known_value is not None:
            inst.last_known_value.accept(self)
        if inst.extra_attrs:
            for v in inst.extra_attrs.attrs.values():
                v.accept(self)

    def visit_type_alias_type(self, t: TypeAliasType) -> None:
        type_ref = t.type_ref
        if type_ref is None:
            return  # We've already been here.
        t.type_ref = None
        t.alias = lookup_fully_qualified_alias(
            self.modules, type_ref, allow_missing=self.allow_missing
        )
        for a in t.args:
            a.accept(self)

    def visit_any(self, o: Any) -> None:
        pass  # Nothing to descend into.

    def visit_callable_type(self, ct: CallableType) -> None:
        if ct.fallback:
            ct.fallback.accept(self)
        for argt in ct.arg_types:
            # argt may be None, e.g. for __self in NamedTuple constructors.
            if argt is not None:
                argt.accept(self)
        if ct.ret_type is not None:
            ct.ret_type.accept(self)
        for v in ct.variables:
            v.accept(self)
        for arg in ct.bound_args:
            if arg:
                arg.accept(self)
        if ct.type_guard is not None:
            ct.type_guard.accept(self)
        if ct.type_is is not None:
            ct.type_is.accept(self)

    def visit_overloaded(self, t: Overloaded) -> None:
        for ct in t.items:
            ct.accept(self)

    def visit_erased_type(self, o: Any) -> None:
        # This type should exist only temporarily during type inference
        raise RuntimeError("Shouldn't get here", o)

    def visit_deleted_type(self, o: Any) -> None:
        pass  # Nothing to descend into.

    def visit_none_type(self, o: Any) -> None:
        pass  # Nothing to descend into.

    def visit_uninhabited_type(self, o: Any) -> None:
        pass  # Nothing to descend into.

    def visit_partial_type(self, o: Any) -> None:
        raise RuntimeError("Shouldn't get here", o)

    def visit_tuple_type(self, tt: TupleType) -> None:
        if tt.items:
            for it in tt.items:
                it.accept(self)
        if tt.partial_fallback is not None:
            tt.partial_fallback.accept(self)

    def visit_typeddict_type(self, tdt: TypedDictType) -> None:
        if tdt.items:
            for it in tdt.items.values():
                it.accept(self)
        if tdt.fallback is not None:
            if tdt.fallback.type_ref is not None:
                if (
                    lookup_fully_qualified(
                        tdt.fallback.type_ref,
                        self.modules,
                        raise_on_missing=not self.allow_missing,
                    )
                    is None
                ):
                    # We reject fake TypeInfos for TypedDict fallbacks because
                    # the latter are used in type checking and must be valid.
                    tdt.fallback.type_ref = "typing._TypedDict"
            tdt.fallback.accept(self)

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

    def visit_type_var(self, tvt: TypeVarType) -> None:
        if tvt.values:
            for vt in tvt.values:
                vt.accept(self)
        tvt.upper_bound.accept(self)
        tvt.default.accept(self)

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

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

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

    def visit_parameters(self, p: Parameters) -> None:
        for argt in p.arg_types:
            if argt is not None:
                argt.accept(self)
        for var in p.variables:
            var.accept(self)

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

    def visit_union_type(self, ut: UnionType) -> None:
        if ut.items:
            for it in ut.items:
                it.accept(self)

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

def lookup_fully_qualified_typeinfo(
    modules: dict[str, MypyFile], name: str, *, allow_missing: bool
) -> TypeInfo:
    stnode = lookup_fully_qualified(name, modules, raise_on_missing=not allow_missing)
    node = stnode.node if stnode else None
    if isinstance(node, TypeInfo):
        return node
    else:
        # Looks like a missing TypeInfo during an initial daemon load, put something there
        assert (
            allow_missing
        ), "Should never get here in normal mode, got {}:{} instead of TypeInfo".format(
            type(node).__name__, node.fullname if node else ""
        )
        return missing_info(modules)

def lookup_fully_qualified_alias(
    modules: dict[str, MypyFile], name: str, *, allow_missing: bool
) -> TypeAlias:
    stnode = lookup_fully_qualified(name, modules, raise_on_missing=not allow_missing)
    node = stnode.node if stnode else None
    if isinstance(node, TypeAlias):
        return node
    elif isinstance(node, TypeInfo):
        if node.special_alias:
            # Already fixed up.
            return node.special_alias
        if node.tuple_type:
            alias = TypeAlias.from_tuple_type(node)
        elif node.typeddict_type:
            alias = TypeAlias.from_typeddict_type(node)
        else:
            assert allow_missing
            return missing_alias()
        node.special_alias = alias
        return alias
    else:
        # Looks like a missing TypeAlias during an initial daemon load, put something there
        assert (
            allow_missing
        ), "Should never get here in normal mode, got {}:{} instead of TypeAlias".format(
            type(node).__name__, node.fullname if node else ""
        )
        return missing_alias()

_SUGGESTION: Final = "<missing {}: *should* have gone away during fine-grained update>"

def missing_info(modules: dict[str, MypyFile]) -> TypeInfo:
    suggestion = _SUGGESTION.format("info")
    dummy_def = ClassDef(suggestion, Block([]))
    dummy_def.fullname = suggestion

    info = TypeInfo(SymbolTable(), dummy_def, "<missing>")
    obj_type = lookup_fully_qualified_typeinfo(modules, "builtins.object", allow_missing=False)
    info.bases = [Instance(obj_type, [])]
    info.mro = [info, obj_type]
    return info

def missing_alias() -> TypeAlias:
    suggestion = _SUGGESTION.format("alias")
    return TypeAlias(AnyType(TypeOfAny.special_form), suggestion, line=-1, column=-1)


"""Generic node traverser visitor"""

from __future__ import annotations

from mypy.nodes import Block, MypyFile
from mypy.traverser import TraverserVisitor

class TreeFreer(TraverserVisitor):
    def visit_block(self, block: Block) -> None:
        super().visit_block(block)
        block.body.clear()

def free_tree(tree: MypyFile) -> None:
    """Free all the ASTs associated with a module.

    This needs to be done recursively, since symbol tables contain
    references to definitions, so those won't be freed but we want their
    contents to be.
    """
    tree.accept(TreeFreer())
    tree.defs.clear()


"""Interface for accessing the file system with automatic caching.

The idea is to cache the results of any file system state reads during
a single transaction. This has two main benefits:

* This avoids redundant syscalls, as we won't perform the same OS
  operations multiple times.

* This makes it easier to reason about concurrent FS updates, as different
  operations targeting the same paths can't report different state during
  a transaction.

Note that this only deals with reading state, not writing.

Properties maintained by the API:

* The contents of the file are always from the same or later time compared
  to the reported mtime of the file, even if mtime is queried after reading
  a file.

* Repeating an operation produces the same result as the first one during
  a transaction.

* Call flush() to start a new transaction (flush the caches).

The API is a bit limited. It's easy to add new cached operations, however.
You should perform all file system reads through the API to actually take
advantage of the benefits.
"""

from __future__ import annotations

import os
import stat

from mypy_extensions import mypyc_attr

from mypy.util import hash_digest

@mypyc_attr(allow_interpreted_subclasses=True)  # for tests
class FileSystemCache:
    def __init__(self) -> None:
        # The package root is not flushed with the caches.
        # It is set by set_package_root() below.
        self.package_root: list[str] = []
        self.flush()

    def set_package_root(self, package_root: list[str]) -> None:
        self.package_root = package_root

    def flush(self) -> None:
        """Start another transaction and empty all caches."""
        self.stat_or_none_cache: dict[str, os.stat_result | None] = {}

        self.listdir_cache: dict[str, list[str]] = {}
        self.listdir_error_cache: dict[str, OSError] = {}
        self.isfile_case_cache: dict[str, bool] = {}
        self.exists_case_cache: dict[str, bool] = {}
        self.read_cache: dict[str, bytes] = {}
        self.read_error_cache: dict[str, Exception] = {}
        self.hash_cache: dict[str, str] = {}
        self.fake_package_cache: set[str] = set()

    def stat_or_none(self, path: str) -> os.stat_result | None:
        if path in self.stat_or_none_cache:
            return self.stat_or_none_cache[path]

        st = None
        try:
            st = os.stat(path)
        except OSError:
            if self.init_under_package_root(path):
                try:
                    st = self._fake_init(path)
                except OSError:
                    pass

        self.stat_or_none_cache[path] = st
        return st

    def init_under_package_root(self, path: str) -> bool:
        """Is this path an __init__.py under a package root?

        This is used to detect packages that don't contain __init__.py
        files, which is needed to support Bazel.  The function should
        only be called for non-existing files.

        It will return True if it refers to a __init__.py file that
        Bazel would create, so that at runtime Python would think the
        directory containing it is a package.  For this to work you
        must pass one or more package roots using the --package-root
        flag.

        As an exceptional case, any directory that is a package root
        itself will not be considered to contain a __init__.py file.
        This is different from the rules Bazel itself applies, but is
        necessary for mypy to properly distinguish packages from other
        directories.

        See https://docs.bazel.build/versions/master/be/python.html,
        where this behavior is described under legacy_create_init.
        """
        if not self.package_root:
            return False
        dirname, basename = os.path.split(path)
        if basename != "__init__.py":
            return False
        if not os.path.basename(dirname).isidentifier():
            # Can't put an __init__.py in a place that's not an identifier
            return False

        st = self.stat_or_none(dirname)
        if st is None:
            return False
        else:
            if not stat.S_ISDIR(st.st_mode):
                return False
        ok = False
        drive, path = os.path.splitdrive(path)  # Ignore Windows drive name
        if os.path.isabs(path):
            path = os.path.relpath(path)
        path = os.path.normpath(path)
        for root in self.package_root:
            if path.startswith(root):
                if path == root + basename:
                    # A package root itself is never a package.
                    ok = False
                    break
                else:
                    ok = True
        return ok

    def _fake_init(self, path: str) -> os.stat_result:
        """Prime the cache with a fake __init__.py file.

        This makes code that looks for path believe an empty file by
        that name exists.  Should only be called after
        init_under_package_root() returns True.
        """
        dirname, basename = os.path.split(path)
        assert basename == "__init__.py", path
        assert not os.path.exists(path), path  # Not cached!
        dirname = os.path.normpath(dirname)
        st = os.stat(dirname)  # May raise OSError
        # Get stat result as a list so we can modify it.
        seq: list[float] = list(st)
        seq[stat.ST_MODE] = stat.S_IFREG | 0o444
        seq[stat.ST_INO] = 1
        seq[stat.ST_NLINK] = 1
        seq[stat.ST_SIZE] = 0
        st = os.stat_result(seq)
        # Make listdir() and read() also pretend this file exists.
        self.fake_package_cache.add(dirname)
        return st

    def listdir(self, path: str) -> list[str]:
        path = os.path.normpath(path)
        if path in self.listdir_cache:
            res = self.listdir_cache[path]
            # Check the fake cache.
            if path in self.fake_package_cache and "__init__.py" not in res:
                res.append("__init__.py")  # Updates the result as well as the cache
            return res
        if path in self.listdir_error_cache:
            raise copy_os_error(self.listdir_error_cache[path])
        try:
            results = os.listdir(path)
        except OSError as err:
            # Like above, take a copy to reduce memory use.
            self.listdir_error_cache[path] = copy_os_error(err)
            raise err
        self.listdir_cache[path] = results
        # Check the fake cache.
        if path in self.fake_package_cache and "__init__.py" not in results:
            results.append("__init__.py")
        return results

    def isfile(self, path: str) -> bool:
        st = self.stat_or_none(path)
        if st is None:
            return False
        return stat.S_ISREG(st.st_mode)

    def isfile_case(self, path: str, prefix: str) -> bool:
        """Return whether path exists and is a file.

        On case-insensitive filesystems (like Mac or Windows) this returns
        False if the case of path's last component does not exactly match
        the case found in the filesystem.

        We check also the case of other path components up to prefix.
        For example, if path is 'user-stubs/pack/mod.pyi' and prefix is 'user-stubs',
        we check that the case of 'pack' and 'mod.py' matches exactly, 'user-stubs' will be
        case insensitive on case insensitive filesystems.

        The caller must ensure that prefix is a valid file system prefix of path.
        """
        if not self.isfile(path):
            # Fast path
            return False
        if path in self.isfile_case_cache:
            return self.isfile_case_cache[path]
        head, tail = os.path.split(path)
        if not tail:
            self.isfile_case_cache[path] = False
            return False
        try:
            names = self.listdir(head)
            # This allows one to check file name case sensitively in
            # case-insensitive filesystems.
            res = tail in names
        except OSError:
            res = False
        if res:
            # Also recursively check the other path components in case sensitive way.
            res = self.exists_case(head, prefix)
        self.isfile_case_cache[path] = res
        return res

    def exists_case(self, path: str, prefix: str) -> bool:
        """Return whether path exists - checking path components in case sensitive
        fashion, up to prefix.
        """
        if path in self.exists_case_cache:
            return self.exists_case_cache[path]
        head, tail = os.path.split(path)
        if not head.startswith(prefix) or not tail:
            # Only perform the check for paths under prefix.
            self.exists_case_cache[path] = True
            return True
        try:
            names = self.listdir(head)
            # This allows one to check file name case sensitively in
            # case-insensitive filesystems.
            res = tail in names
        except OSError:
            res = False
        if res:
            # Also recursively check other path components.
            res = self.exists_case(head, prefix)
        self.exists_case_cache[path] = res
        return res

    def isdir(self, path: str) -> bool:
        st = self.stat_or_none(path)
        if st is None:
            return False
        return stat.S_ISDIR(st.st_mode)

    def exists(self, path: str) -> bool:
        st = self.stat_or_none(path)
        return st is not None

    def read(self, path: str) -> bytes:
        if path in self.read_cache:
            return self.read_cache[path]
        if path in self.read_error_cache:
            raise self.read_error_cache[path]

        # Need to stat first so that the contents of file are from no
        # earlier instant than the mtime reported by self.stat().
        self.stat_or_none(path)

        dirname, basename = os.path.split(path)
        dirname = os.path.normpath(dirname)
        # Check the fake cache.
        if basename == "__init__.py" and dirname in self.fake_package_cache:
            data = b""
        else:
            try:
                with open(path, "rb") as f:
                    data = f.read()
            except OSError as err:
                self.read_error_cache[path] = err
                raise

        self.read_cache[path] = data
        self.hash_cache[path] = hash_digest(data)
        return data

    def hash_digest(self, path: str) -> str:
        if path not in self.hash_cache:
            self.read(path)
        return self.hash_cache[path]

    def samefile(self, f1: str, f2: str) -> bool:
        s1 = self.stat_or_none(f1)
        s2 = self.stat_or_none(f2)
        if s1 is None or s2 is None:
            return False
        return os.path.samestat(s1, s2)

def copy_os_error(e: OSError) -> OSError:
    new = OSError(*e.args)
    new.errno = e.errno
    new.strerror = e.strerror
    new.filename = e.filename
    if e.filename2:
        new.filename2 = e.filename2
    return new


"""Watch parts of the file system for changes."""

from __future__ import annotations

import os
from typing import AbstractSet, Iterable, NamedTuple

from mypy.fscache import FileSystemCache

class FileData(NamedTuple):
    st_mtime: float
    st_size: int
    hash: str

class FileSystemWatcher:
    """Watcher for file system changes among specific paths.

    All file system access is performed using FileSystemCache. We
    detect changed files by stat()ing them all and comparing hashes
    of potentially changed files. If a file has both size and mtime
    unmodified, the file is assumed to be unchanged.

    An important goal of this class is to make it easier to eventually
    use file system events to detect file changes.

    Note: This class doesn't flush the file system cache. If you don't
    manually flush it, changes won't be seen.
    """

    # TODO: Watching directories?
    # TODO: Handle non-files

    def __init__(self, fs: FileSystemCache) -> None:
        self.fs = fs
        self._paths: set[str] = set()
        self._file_data: dict[str, FileData | None] = {}

    def dump_file_data(self) -> dict[str, tuple[float, int, str]]:
        return {k: v for k, v in self._file_data.items() if v is not None}

    def set_file_data(self, path: str, data: FileData) -> None:
        self._file_data[path] = data

    def add_watched_paths(self, paths: Iterable[str]) -> None:
        for path in paths:
            if path not in self._paths:
                # By storing None this path will get reported as changed by
                # find_changed if it exists.
                self._file_data[path] = None
        self._paths |= set(paths)

    def remove_watched_paths(self, paths: Iterable[str]) -> None:
        for path in paths:
            if path in self._file_data:
                del self._file_data[path]
        self._paths -= set(paths)

    def _update(self, path: str, st: os.stat_result) -> None:
        hash_digest = self.fs.hash_digest(path)
        self._file_data[path] = FileData(st.st_mtime, st.st_size, hash_digest)

    def _find_changed(self, paths: Iterable[str]) -> AbstractSet[str]:
        changed = set()
        for path in paths:
            old = self._file_data[path]
            st = self.fs.stat_or_none(path)
            if st is None:
                if old is not None:
                    # File was deleted.
                    changed.add(path)
                    self._file_data[path] = None
            else:
                if old is None:
                    # File is new.
                    changed.add(path)
                    self._update(path, st)
                # Round mtimes down, to match the mtimes we write to meta files
                elif st.st_size != old.st_size or int(st.st_mtime) != int(old.st_mtime):
                    # Only look for changes if size or mtime has changed as an
                    # optimization, since calculating hash is expensive.
                    new_hash = self.fs.hash_digest(path)
                    self._update(path, st)
                    if st.st_size != old.st_size or new_hash != old.hash:
                        # Changed file.
                        changed.add(path)
        return changed

    def find_changed(self) -> AbstractSet[str]:
        """Return paths that have changes since the last call, in the watched set."""
        return self._find_changed(self._paths)

    def update_changed(self, remove: list[str], update: list[str]) -> AbstractSet[str]:
        """Alternative to find_changed() given explicit changes.

        This only calls self.fs.stat() on added or updated files, not
        on all files.  It believes all other files are unchanged!

        Implies add_watched_paths() for add and update, and
        remove_watched_paths() for remove.
        """
        self.remove_watched_paths(remove)
        self.add_watched_paths(update)
        return self._find_changed(update)


from __future__ import annotations

import gc
import time
from typing import Mapping

class GcLogger:
    """Context manager to log GC stats and overall time."""

    def __enter__(self) -> GcLogger:
        self.gc_start_time: float | None = None
        self.gc_time = 0.0
        self.gc_calls = 0
        self.gc_collected = 0
        self.gc_uncollectable = 0
        gc.callbacks.append(self.gc_callback)
        self.start_time = time.time()
        return self

    def gc_callback(self, phase: str, info: Mapping[str, int]) -> None:
        if phase == "start":
            assert self.gc_start_time is None, "Start phase out of sequence"
            self.gc_start_time = time.time()
        elif phase == "stop":
            assert self.gc_start_time is not None, "Stop phase out of sequence"
            self.gc_calls += 1
            self.gc_time += time.time() - self.gc_start_time
            self.gc_start_time = None
            self.gc_collected += info["collected"]
            self.gc_uncollectable += info["uncollectable"]
        else:
            assert False, f"Unrecognized gc phase ({phase!r})"

    def __exit__(self, *args: object) -> None:
        while self.gc_callback in gc.callbacks:
            gc.callbacks.remove(self.gc_callback)

    def get_stats(self) -> Mapping[str, float]:
        end_time = time.time()
        result = {}
        result["gc_time"] = self.gc_time
        result["gc_calls"] = self.gc_calls
        result["gc_collected"] = self.gc_collected
        result["gc_uncollectable"] = self.gc_uncollectable
        result["build_time"] = end_time - self.start_time
        return result


"""Git utilities."""

# Used also from setup.py, so don't pull in anything additional here (like mypy or typing):
from __future__ import annotations

import os
import subprocess

def is_git_repo(dir: str) -> bool:
    """Is the given directory version-controlled with git?"""
    return os.path.exists(os.path.join(dir, ".git"))

def have_git() -> bool:
    """Can we run the git executable?"""
    try:
        subprocess.check_output(["git", "--help"])
        return True
    except subprocess.CalledProcessError:
        return False
    except OSError:
        return False

def git_revision(dir: str) -> bytes:
    """Get the SHA-1 of the HEAD of a git repository."""
    return subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=dir).strip()

def is_dirty(dir: str) -> bool:
    """Check whether a git repository has uncommitted changes."""
    output = subprocess.check_output(["git", "status", "-uno", "--porcelain"], cwd=dir)
    return output.strip() != b""


"""Helpers for manipulations with graphs."""

from __future__ import annotations

from typing import AbstractSet, Iterable, Iterator, TypeVar

T = TypeVar("T")

def strongly_connected_components(
    vertices: AbstractSet[T], edges: dict[T, list[T]]
) -> Iterator[set[T]]:
    """Compute Strongly Connected Components of a directed graph.

    Args:
      vertices: the labels for the vertices
      edges: for each vertex, gives the target vertices of its outgoing edges

    Returns:
      An iterator yielding strongly connected components, each
      represented as a set of vertices.  Each input vertex will occur
      exactly once; vertices not part of a SCC are returned as
      singleton sets.

    From https://code.activestate.com/recipes/578507/.
    """
    identified: set[T] = set()
    stack: list[T] = []
    index: dict[T, int] = {}
    boundaries: list[int] = []

    def dfs(v: T) -> Iterator[set[T]]:
        index[v] = len(stack)
        stack.append(v)
        boundaries.append(index[v])

        for w in edges[v]:
            if w not in index:
                yield from dfs(w)
            elif w not in identified:
                while index[w] < boundaries[-1]:
                    boundaries.pop()

        if boundaries[-1] == index[v]:
            boundaries.pop()
            scc = set(stack[index[v] :])
            del stack[index[v] :]
            identified.update(scc)
            yield scc

    for v in vertices:
        if v not in index:
            yield from dfs(v)

def prepare_sccs(
    sccs: list[set[T]], edges: dict[T, list[T]]
) -> dict[AbstractSet[T], set[AbstractSet[T]]]:
    """Use original edges to organize SCCs in a graph by dependencies between them."""
    sccsmap = {v: frozenset(scc) for scc in sccs for v in scc}
    data: dict[AbstractSet[T], set[AbstractSet[T]]] = {}
    for scc in sccs:
        deps: set[AbstractSet[T]] = set()
        for v in scc:
            deps.update(sccsmap[x] for x in edges[v])
        data[frozenset(scc)] = deps
    return data

def topsort(data: dict[T, set[T]]) -> Iterable[set[T]]:
    """Topological sort.

    Args:
      data: A map from vertices to all vertices that it has an edge
            connecting it to.  NOTE: This data structure
            is modified in place -- for normalization purposes,
            self-dependencies are removed and entries representing
            orphans are added.

    Returns:
      An iterator yielding sets of vertices that have an equivalent
      ordering.

    Example:
      Suppose the input has the following structure:

        {A: {B, C}, B: {D}, C: {D}}

      This is normalized to:

        {A: {B, C}, B: {D}, C: {D}, D: {}}

      The algorithm will yield the following values:

        {D}
        {B, C}
        {A}

    From https://code.activestate.com/recipes/577413/.
    """
    # TODO: Use a faster algorithm?
    for k, v in data.items():
        v.discard(k)  # Ignore self dependencies.
    for item in set.union(*data.values()) - set(data.keys()):
        data[item] = set()
    while True:
        ready = {item for item, dep in data.items() if not dep}
        if not ready:
            break
        yield ready
        data = {item: (dep - ready) for item, dep in data.items() if item not in ready}
    assert not data, f"A cyclic dependency exists amongst {data!r}"


from __future__ import annotations

from typing import Iterable, Set

import mypy.types as types
from mypy.types import TypeVisitor
from mypy.util import split_module_names

def extract_module_names(type_name: str | None) -> list[str]:
    """Returns the module names of a fully qualified type name."""
    if type_name is not None:
        # Discard the first one, which is just the qualified name of the type
        possible_module_names = split_module_names(type_name)
        return possible_module_names[1:]
    else:
        return []

class TypeIndirectionVisitor(TypeVisitor[Set[str]]):
    """Returns all module references within a particular type."""

    def __init__(self) -> None:
        self.cache: dict[types.Type, set[str]] = {}
        self.seen_aliases: set[types.TypeAliasType] = set()

    def find_modules(self, typs: Iterable[types.Type]) -> set[str]:
        self.seen_aliases.clear()
        return self._visit(typs)

    def _visit(self, typ_or_typs: types.Type | Iterable[types.Type]) -> set[str]:
        typs = [typ_or_typs] if isinstance(typ_or_typs, types.Type) else typ_or_typs
        output: set[str] = set()
        for typ in typs:
            if isinstance(typ, types.TypeAliasType):
                # Avoid infinite recursion for recursive type aliases.
                if typ in self.seen_aliases:
                    continue
                self.seen_aliases.add(typ)
            if typ in self.cache:
                modules = self.cache[typ]
            else:
                modules = typ.accept(self)
                self.cache[typ] = set(modules)
            output.update(modules)
        return output

    def visit_unbound_type(self, t: types.UnboundType) -> set[str]:
        return self._visit(t.args)

    def visit_any(self, t: types.AnyType) -> set[str]:
        return set()

    def visit_none_type(self, t: types.NoneType) -> set[str]:
        return set()

    def visit_uninhabited_type(self, t: types.UninhabitedType) -> set[str]:
        return set()

    def visit_erased_type(self, t: types.ErasedType) -> set[str]:
        return set()

    def visit_deleted_type(self, t: types.DeletedType) -> set[str]:
        return set()

    def visit_type_var(self, t: types.TypeVarType) -> set[str]:
        return self._visit(t.values) | self._visit(t.upper_bound) | self._visit(t.default)

    def visit_param_spec(self, t: types.ParamSpecType) -> set[str]:
        return self._visit(t.upper_bound) | self._visit(t.default)

    def visit_type_var_tuple(self, t: types.TypeVarTupleType) -> set[str]:
        return self._visit(t.upper_bound) | self._visit(t.default)

    def visit_unpack_type(self, t: types.UnpackType) -> set[str]:
        return t.type.accept(self)

    def visit_parameters(self, t: types.Parameters) -> set[str]:
        return self._visit(t.arg_types)

    def visit_instance(self, t: types.Instance) -> set[str]:
        out = self._visit(t.args)
        if t.type:
            # Uses of a class depend on everything in the MRO,
            # as changes to classes in the MRO can add types to methods,
            # change property types, change the MRO itself, etc.
            for s in t.type.mro:
                out.update(split_module_names(s.module_name))
            if t.type.metaclass_type is not None:
                out.update(split_module_names(t.type.metaclass_type.type.module_name))
        return out

    def visit_callable_type(self, t: types.CallableType) -> set[str]:
        out = self._visit(t.arg_types) | self._visit(t.ret_type)
        if t.definition is not None:
            out.update(extract_module_names(t.definition.fullname))
        return out

    def visit_overloaded(self, t: types.Overloaded) -> set[str]:
        return self._visit(t.items) | self._visit(t.fallback)

    def visit_tuple_type(self, t: types.TupleType) -> set[str]:
        return self._visit(t.items) | self._visit(t.partial_fallback)

    def visit_typeddict_type(self, t: types.TypedDictType) -> set[str]:
        return self._visit(t.items.values()) | self._visit(t.fallback)

    def visit_literal_type(self, t: types.LiteralType) -> set[str]:
        return self._visit(t.fallback)

    def visit_union_type(self, t: types.UnionType) -> set[str]:
        return self._visit(t.items)

    def visit_partial_type(self, t: types.PartialType) -> set[str]:
        return set()

    def visit_type_type(self, t: types.TypeType) -> set[str]:
        return self._visit(t.item)

    def visit_type_alias_type(self, t: types.TypeAliasType) -> set[str]:
        return self._visit(types.get_proper_type(t))


"""Utilities for type argument inference."""

from __future__ import annotations

from typing import NamedTuple, Sequence

from mypy.constraints import (
    SUBTYPE_OF,
    SUPERTYPE_OF,
    infer_constraints,
    infer_constraints_for_callable,
)
from mypy.nodes import ArgKind
from mypy.solve import solve_constraints
from mypy.types import CallableType, Instance, Type, TypeVarLikeType

class ArgumentInferContext(NamedTuple):
    """Type argument inference context.

    We need this because we pass around ``Mapping`` and ``Iterable`` types.
    These types are only known by ``TypeChecker`` itself.
    It is required for ``*`` and ``**`` argument inference.

    https://github.com/python/mypy/issues/11144
    """

    mapping_type: Instance
    iterable_type: Instance

def infer_function_type_arguments(
    callee_type: CallableType,
    arg_types: Sequence[Type | None],
    arg_kinds: list[ArgKind],
    arg_names: Sequence[str | None] | None,
    formal_to_actual: list[list[int]],
    context: ArgumentInferContext,
    strict: bool = True,
    allow_polymorphic: bool = False,
) -> tuple[list[Type | None], list[TypeVarLikeType]]:
    """Infer the type arguments of a generic function.

    Return an array of lower bound types for the type variables -1 (at
    index 0), -2 (at index 1), etc. A lower bound is None if a value
    could not be inferred.

    Arguments:
      callee_type: the target generic function
      arg_types: argument types at the call site (each optional; if None,
                 we are not considering this argument in the current pass)
      arg_kinds: nodes.ARG_* values for arg_types
      formal_to_actual: mapping from formal to actual variable indices
    """
    # Infer constraints.
    constraints = infer_constraints_for_callable(
        callee_type, arg_types, arg_kinds, arg_names, formal_to_actual, context
    )

    # Solve constraints.
    type_vars = callee_type.variables
    return solve_constraints(type_vars, constraints, strict, allow_polymorphic)

def infer_type_arguments(
    type_vars: Sequence[TypeVarLikeType],
    template: Type,
    actual: Type,
    is_supertype: bool = False,
    skip_unsatisfied: bool = False,
) -> list[Type | None]:
    # Like infer_function_type_arguments, but only match a single type
    # against a generic type.
    constraints = infer_constraints(template, actual, SUPERTYPE_OF if is_supertype else SUBTYPE_OF)
    return solve_constraints(type_vars, constraints, skip_unsatisfied=skip_unsatisfied)[0]


from __future__ import annotations

import os
from collections import defaultdict
from functools import cmp_to_key
from typing import Callable

from mypy.build import State
from mypy.messages import format_type
from mypy.modulefinder import PYTHON_EXTENSIONS
from mypy.nodes import (
    LDEF,
    Decorator,
    Expression,
    FuncBase,
    MemberExpr,
    MypyFile,
    Node,
    OverloadedFuncDef,
    RefExpr,
    SymbolNode,
    TypeInfo,
    Var,
)
from mypy.server.update import FineGrainedBuildManager
from mypy.traverser import ExtendedTraverserVisitor
from mypy.typeops import tuple_fallback
from mypy.types import (
    FunctionLike,
    Instance,
    LiteralType,
    ProperType,
    TupleType,
    TypedDictType,
    TypeVarType,
    UnionType,
    get_proper_type,
)
from mypy.typevars import fill_typevars_with_any

def node_starts_after(o: Node, line: int, column: int) -> bool:
    return o.line > line or o.line == line and o.column > column

def node_ends_before(o: Node, line: int, column: int) -> bool:
    # Unfortunately, end positions for some statements are a mess,
    # e.g. overloaded functions, so we return False when we don't know.
    if o.end_line is not None and o.end_column is not None:
        if o.end_line < line or o.end_line == line and o.end_column < column:
            return True
    return False

def expr_span(expr: Expression) -> str:
    """Format expression span as in mypy error messages."""
    return f"{expr.line}:{expr.column + 1}:{expr.end_line}:{expr.end_column}"

def get_instance_fallback(typ: ProperType) -> list[Instance]:
    """Returns the Instance fallback for this type if one exists or None."""
    if isinstance(typ, Instance):
        return [typ]
    elif isinstance(typ, TupleType):
        return [tuple_fallback(typ)]
    elif isinstance(typ, TypedDictType):
        return [typ.fallback]
    elif isinstance(typ, FunctionLike):
        return [typ.fallback]
    elif isinstance(typ, LiteralType):
        return [typ.fallback]
    elif isinstance(typ, TypeVarType):
        if typ.values:
            res = []
            for t in typ.values:
                res.extend(get_instance_fallback(get_proper_type(t)))
            return res
        return get_instance_fallback(get_proper_type(typ.upper_bound))
    elif isinstance(typ, UnionType):
        res = []
        for t in typ.items:
            res.extend(get_instance_fallback(get_proper_type(t)))
        return res
    return []

def find_node(name: str, info: TypeInfo) -> Var | FuncBase | None:
    """Find the node defining member 'name' in given TypeInfo."""
    # TODO: this code shares some logic with checkmember.py
    method = info.get_method(name)
    if method:
        if isinstance(method, Decorator):
            return method.var
        if method.is_property:
            assert isinstance(method, OverloadedFuncDef)
            dec = method.items[0]
            assert isinstance(dec, Decorator)
            return dec.var
        return method
    else:
        # don't have such method, maybe variable?
        node = info.get(name)
        v = node.node if node else None
        if isinstance(v, Var):
            return v
    return None

def find_module_by_fullname(fullname: str, modules: dict[str, State]) -> State | None:
    """Find module by a node fullname.

    This logic mimics the one we use in fixup, so should be good enough.
    """
    head = fullname
    # Special case: a module symbol is considered to be defined in itself, not in enclosing
    # package, since this is what users want when clicking go to definition on a module.
    if head in modules:
        return modules[head]
    while True:
        if "." not in head:
            return None
        head, tail = head.rsplit(".", maxsplit=1)
        mod = modules.get(head)
        if mod is not None:
            return mod

class SearchVisitor(ExtendedTraverserVisitor):
    """Visitor looking for an expression whose span matches given one exactly."""

    def __init__(self, line: int, column: int, end_line: int, end_column: int) -> None:
        self.line = line
        self.column = column
        self.end_line = end_line
        self.end_column = end_column
        self.result: Expression | None = None

    def visit(self, o: Node) -> bool:
        if node_starts_after(o, self.line, self.column):
            return False
        if node_ends_before(o, self.end_line, self.end_column):
            return False
        if (
            o.line == self.line
            and o.end_line == self.end_line
            and o.column == self.column
            and o.end_column == self.end_column
        ):
            if isinstance(o, Expression):
                self.result = o
        return self.result is None

def find_by_location(
    tree: MypyFile, line: int, column: int, end_line: int, end_column: int
) -> Expression | None:
    """Find an expression matching given span, or None if not found."""
    if end_line < line:
        raise ValueError('"end_line" must not be before "line"')
    if end_line == line and end_column <= column:
        raise ValueError('"end_column" must be after "column"')
    visitor = SearchVisitor(line, column, end_line, end_column)
    tree.accept(visitor)
    return visitor.result

class SearchAllVisitor(ExtendedTraverserVisitor):
    """Visitor looking for all expressions whose spans enclose given position."""

    def __init__(self, line: int, column: int) -> None:
        self.line = line
        self.column = column
        self.result: list[Expression] = []

    def visit(self, o: Node) -> bool:
        if node_starts_after(o, self.line, self.column):
            return False
        if node_ends_before(o, self.line, self.column):
            return False
        if isinstance(o, Expression):
            self.result.append(o)
        return True

def find_all_by_location(tree: MypyFile, line: int, column: int) -> list[Expression]:
    """Find all expressions enclosing given position starting from innermost."""
    visitor = SearchAllVisitor(line, column)
    tree.accept(visitor)
    return list(reversed(visitor.result))

class InspectionEngine:
    """Engine for locating and statically inspecting expressions."""

    def __init__(
        self,
        fg_manager: FineGrainedBuildManager,
        *,
        verbosity: int = 0,
        limit: int = 0,
        include_span: bool = False,
        include_kind: bool = False,
        include_object_attrs: bool = False,
        union_attrs: bool = False,
        force_reload: bool = False,
    ) -> None:
        self.fg_manager = fg_manager
        self.verbosity = verbosity
        self.limit = limit
        self.include_span = include_span
        self.include_kind = include_kind
        self.include_object_attrs = include_object_attrs
        self.union_attrs = union_attrs
        self.force_reload = force_reload
        # Module for which inspection was requested.
        self.module: State | None = None

    def reload_module(self, state: State) -> None:
        """Reload given module while temporary exporting types."""
        old = self.fg_manager.manager.options.export_types
        self.fg_manager.manager.options.export_types = True
        try:
            self.fg_manager.flush_cache()
            assert state.path is not None
            self.fg_manager.update([(state.id, state.path)], [])
        finally:
            self.fg_manager.manager.options.export_types = old

    def expr_type(self, expression: Expression) -> tuple[str, bool]:
        """Format type for an expression using current options.

        If type is known, second item returned is True. If type is not known, an error
        message is returned instead, and second item returned is False.
        """
        expr_type = self.fg_manager.manager.all_types.get(expression)
        if expr_type is None:
            return self.missing_type(expression), False

        type_str = format_type(
            expr_type, self.fg_manager.manager.options, verbosity=self.verbosity
        )
        return self.add_prefixes(type_str, expression), True

    def object_type(self) -> Instance:
        builtins = self.fg_manager.graph["builtins"].tree
        assert builtins is not None
        object_node = builtins.names["object"].node
        assert isinstance(object_node, TypeInfo)
        return Instance(object_node, [])

    def collect_attrs(self, instances: list[Instance]) -> dict[TypeInfo, list[str]]:
        """Collect attributes from all union/typevar variants."""

        def item_attrs(attr_dict: dict[TypeInfo, list[str]]) -> set[str]:
            attrs = set()
            for base in attr_dict:
                attrs |= set(attr_dict[base])
            return attrs

        def cmp_types(x: TypeInfo, y: TypeInfo) -> int:
            if x in y.mro:
                return 1
            if y in x.mro:
                return -1
            return 0

        # First gather all attributes for every union variant.
        assert instances
        all_attrs = []
        for instance in instances:
            attrs = {}
            mro = instance.type.mro
            if not self.include_object_attrs:
                mro = mro[:-1]
            for base in mro:
                attrs[base] = sorted(base.names)
            all_attrs.append(attrs)

        # Find attributes valid for all variants in a union or type variable.
        intersection = item_attrs(all_attrs[0])
        for item in all_attrs[1:]:
            intersection &= item_attrs(item)

        # Combine attributes from all variants into a single dict while
        # also removing invalid attributes (unless using --union-attrs).
        combined_attrs = defaultdict(list)
        for item in all_attrs:
            for base in item:
                if base in combined_attrs:
                    continue
                for name in item[base]:
                    if self.union_attrs or name in intersection:
                        combined_attrs[base].append(name)

        # Sort bases by MRO, unrelated will appear in the order they appeared as union variants.
        sorted_bases = sorted(combined_attrs.keys(), key=cmp_to_key(cmp_types))
        result = {}
        for base in sorted_bases:
            if not combined_attrs[base]:
                # Skip bases where everytihng was filtered out.
                continue
            result[base] = combined_attrs[base]
        return result

    def _fill_from_dict(
        self, attrs_strs: list[str], attrs_dict: dict[TypeInfo, list[str]]
    ) -> None:
        for base in attrs_dict:
            cls_name = base.name if self.verbosity < 1 else base.fullname
            attrs = [f'"{attr}"' for attr in attrs_dict[base]]
            attrs_strs.append(f'"{cls_name}": [{", ".join(attrs)}]')

    def expr_attrs(self, expression: Expression) -> tuple[str, bool]:
        """Format attributes that are valid for a given expression.

        If expression type is not an Instance, try using fallback. Attributes are
        returned as a JSON (ordered by MRO) that maps base class name to list of
        attributes. Attributes may appear in multiple bases if overridden (we simply
        follow usual mypy logic for creating new Vars etc).
        """
        expr_type = self.fg_manager.manager.all_types.get(expression)
        if expr_type is None:
            return self.missing_type(expression), False

        expr_type = get_proper_type(expr_type)
        instances = get_instance_fallback(expr_type)
        if not instances:
            # Everything is an object in Python.
            instances = [self.object_type()]

        attrs_dict = self.collect_attrs(instances)

        # Special case: modules have names apart from those from ModuleType.
        if isinstance(expression, RefExpr) and isinstance(expression.node, MypyFile):
            node = expression.node
            names = sorted(node.names)
            if "__builtins__" in names:
                # This is just to make tests stable. No one will really need ths name.
                names.remove("__builtins__")
            mod_dict = {f'"<{node.fullname}>"': [f'"{name}"' for name in names]}
        else:
            mod_dict = {}

        # Special case: for class callables, prepend with the class attributes.
        # TODO: also handle cases when such callable appears in a union.
        if isinstance(expr_type, FunctionLike) and expr_type.is_type_obj():
            template = fill_typevars_with_any(expr_type.type_object())
            class_dict = self.collect_attrs(get_instance_fallback(template))
        else:
            class_dict = {}

        # We don't use JSON dump to be sure keys order is always preserved.
        base_attrs = []
        if mod_dict:
            for mod in mod_dict:
                base_attrs.append(f'{mod}: [{", ".join(mod_dict[mod])}]')
        self._fill_from_dict(base_attrs, class_dict)
        self._fill_from_dict(base_attrs, attrs_dict)
        return self.add_prefixes(f'{{{", ".join(base_attrs)}}}', expression), True

    def format_node(self, module: State, node: FuncBase | SymbolNode) -> str:
        return f"{module.path}:{node.line}:{node.column + 1}:{node.name}"

    def collect_nodes(self, expression: RefExpr) -> list[FuncBase | SymbolNode]:
        """Collect nodes that can be referred to by an expression.

        Note: it can be more than one for example in case of a union attribute.
        """
        node: FuncBase | SymbolNode | None = expression.node
        nodes: list[FuncBase | SymbolNode]
        if node is None:
            # Tricky case: instance attribute
            if isinstance(expression, MemberExpr) and expression.kind is None:
                base_type = self.fg_manager.manager.all_types.get(expression.expr)
                if base_type is None:
                    return []

                # Now we use the base type to figure out where the attribute is defined.
                base_type = get_proper_type(base_type)
                instances = get_instance_fallback(base_type)
                nodes = []
                for instance in instances:
                    node = find_node(expression.name, instance.type)
                    if node:
                        nodes.append(node)
                if not nodes:
                    # Try checking class namespace if attribute is on a class object.
                    if isinstance(base_type, FunctionLike) and base_type.is_type_obj():
                        instances = get_instance_fallback(
                            fill_typevars_with_any(base_type.type_object())
                        )
                        for instance in instances:
                            node = find_node(expression.name, instance.type)
                            if node:
                                nodes.append(node)
                    else:
                        # Still no luck, give up.
                        return []
            else:
                return []
        else:
            # Easy case: a module-level definition
            nodes = [node]
        return nodes

    def modules_for_nodes(
        self, nodes: list[FuncBase | SymbolNode], expression: RefExpr
    ) -> tuple[dict[FuncBase | SymbolNode, State], bool]:
        """Gather modules where given nodes where defined.

        Also check if they need to be refreshed (cached nodes may have
        lines/columns missing).
        """
        modules = {}
        reload_needed = False
        for node in nodes:
            module = find_module_by_fullname(node.fullname, self.fg_manager.graph)
            if not module:
                if expression.kind == LDEF and self.module:
                    module = self.module
                else:
                    continue
            modules[node] = module
            if not module.tree or module.tree.is_cache_skeleton or self.force_reload:
                reload_needed |= not module.tree or module.tree.is_cache_skeleton
                self.reload_module(module)
        return modules, reload_needed

    def expression_def(self, expression: Expression) -> tuple[str, bool]:
        """Find and format definition location for an expression.

        If it is not a RefExpr, it is effectively skipped by returning an
        empty result.
        """
        if not isinstance(expression, RefExpr):
            # If there are no suitable matches at all, we return error later.
            return "", True

        nodes = self.collect_nodes(expression)

        if not nodes:
            return self.missing_node(expression), False

        modules, reload_needed = self.modules_for_nodes(nodes, expression)
        if reload_needed:
            # TODO: line/column are not stored in cache for vast majority of symbol nodes.
            # Adding them will make thing faster, but will have visible memory impact.
            nodes = self.collect_nodes(expression)
            modules, reload_needed = self.modules_for_nodes(nodes, expression)
            assert not reload_needed

        result = []
        for node in modules:
            result.append(self.format_node(modules[node], node))

        if not result:
            return self.missing_node(expression), False

        return self.add_prefixes(", ".join(result), expression), True

    def missing_type(self, expression: Expression) -> str:
        alt_suggestion = ""
        if not self.force_reload:
            alt_suggestion = " or try --force-reload"
        return (
            f'No known type available for "{type(expression).__name__}"'
            f" (maybe unreachable{alt_suggestion})"
        )

    def missing_node(self, expression: Expression) -> str:
        return (
            f'Cannot find definition for "{type(expression).__name__}"'
            f" at {expr_span(expression)}"
        )

    def add_prefixes(self, result: str, expression: Expression) -> str:
        prefixes = []
        if self.include_kind:
            prefixes.append(f"{type(expression).__name__}")
        if self.include_span:
            prefixes.append(expr_span(expression))
        if prefixes:
            prefix = ":".join(prefixes) + " -> "
        else:
            prefix = ""
        return prefix + result

    def run_inspection_by_exact_location(
        self,
        tree: MypyFile,
        line: int,
        column: int,
        end_line: int,
        end_column: int,
        method: Callable[[Expression], tuple[str, bool]],
    ) -> dict[str, object]:
        """Get type of an expression matching a span.

        Type or error is returned as a standard daemon response dict.
        """
        try:
            expression = find_by_location(tree, line, column - 1, end_line, end_column)
        except ValueError as err:
            return {"error": str(err)}

        if expression is None:
            span = f"{line}:{column}:{end_line}:{end_column}"
            return {"out": f"Can't find expression at span {span}", "err": "", "status": 1}

        inspection_str, success = method(expression)
        return {"out": inspection_str, "err": "", "status": 0 if success else 1}

    def run_inspection_by_position(
        self,
        tree: MypyFile,
        line: int,
        column: int,
        method: Callable[[Expression], tuple[str, bool]],
    ) -> dict[str, object]:
        """Get types of all expressions enclosing a position.

        Types and/or errors are returned as a standard daemon response dict.
        """
        expressions = find_all_by_location(tree, line, column - 1)
        if not expressions:
            position = f"{line}:{column}"
            return {
                "out": f"Can't find any expressions at position {position}",
                "err": "",
                "status": 1,
            }

        inspection_strs = []
        status = 0
        for expression in expressions:
            inspection_str, success = method(expression)
            if not success:
                status = 1
            if inspection_str:
                inspection_strs.append(inspection_str)
        if self.limit:
            inspection_strs = inspection_strs[: self.limit]
        return {"out": "\n".join(inspection_strs), "err": "", "status": status}

    def find_module(self, file: str) -> tuple[State | None, dict[str, object]]:
        """Find module by path, or return a suitable error message.

        Note we don't use exceptions to simplify handling 1 vs 2 statuses.
        """
        if not any(file.endswith(ext) for ext in PYTHON_EXTENSIONS):
            return None, {"error": "Source file is not a Python file"}

        # We are using a bit slower but robust way to find a module by path,
        # to be sure that namespace packages are handled properly.
        abs_path = os.path.abspath(file)
        state = next((s for s in self.fg_manager.graph.values() if s.abspath == abs_path), None)
        self.module = state
        return (
            state,
            {"out": f"Unknown module: {file}", "err": "", "status": 1} if state is None else {},
        )

    def run_inspection(
        self, location: str, method: Callable[[Expression], tuple[str, bool]]
    ) -> dict[str, object]:
        """Top-level logic to inspect expression(s) at a location.

        This can be re-used by various simple inspections.
        """
        try:
            file, pos = parse_location(location)
        except ValueError as err:
            return {"error": str(err)}

        state, err_dict = self.find_module(file)
        if state is None:
            assert err_dict
            return err_dict

        # Force reloading to load from cache, account for any edits, etc.
        if not state.tree or state.tree.is_cache_skeleton or self.force_reload:
            self.reload_module(state)
        assert state.tree is not None

        if len(pos) == 4:
            # Full span, return an exact match only.
            line, column, end_line, end_column = pos
            return self.run_inspection_by_exact_location(
                state.tree, line, column, end_line, end_column, method
            )
        assert len(pos) == 2
        # Inexact location, return all expressions.
        line, column = pos
        return self.run_inspection_by_position(state.tree, line, column, method)

    def get_type(self, location: str) -> dict[str, object]:
        """Get types of expression(s) at a location."""
        return self.run_inspection(location, self.expr_type)

    def get_attrs(self, location: str) -> dict[str, object]:
        """Get attributes of expression(s) at a location."""
        return self.run_inspection(location, self.expr_attrs)

    def get_definition(self, location: str) -> dict[str, object]:
        """Get symbol definitions of expression(s) at a location."""
        result = self.run_inspection(location, self.expression_def)
        if "out" in result and not result["out"]:
            # None of the expressions found turns out to be a RefExpr.
            _, location = location.split(":", maxsplit=1)
            result["out"] = f"No name or member expressions at {location}"
            result["status"] = 1
        return result

def parse_location(location: str) -> tuple[str, list[int]]:
    if location.count(":") < 2:
        raise ValueError("Format should be file:line:column[:end_line:end_column]")
    parts = location.rsplit(":", maxsplit=2)
    start, *rest = parts
    # Note: we must allow drive prefix like `C:` on Windows.
    if start.count(":") < 2:
        return start, [int(p) for p in rest]
    parts = start.rsplit(":", maxsplit=2)
    start, *start_rest = parts
    if start.count(":") < 2:
        return start, [int(p) for p in start_rest + rest]
    raise ValueError("Format should be file:line:column[:end_line:end_column]")


"""Cross platform abstractions for inter-process communication

On Unix, this uses AF_UNIX sockets.
On Windows, this uses NamedPipes.
"""

from __future__ import annotations

import base64
import codecs
import os
import shutil
import sys
import tempfile
from types import TracebackType
from typing import Callable, Final

if sys.platform == "win32":
    # This may be private, but it is needed for IPC on Windows, and is basically stable
    import ctypes

    import _winapi

    _IPCHandle = int

    kernel32 = ctypes.windll.kernel32
    DisconnectNamedPipe: Callable[[_IPCHandle], int] = kernel32.DisconnectNamedPipe
    FlushFileBuffers: Callable[[_IPCHandle], int] = kernel32.FlushFileBuffers
else:
    import socket

    _IPCHandle = socket.socket

class IPCException(Exception):
    """Exception for IPC issues."""

class IPCBase:
    """Base class for communication between the dmypy client and server.

    This contains logic shared between the client and server, such as reading
    and writing.
    We want to be able to send multiple "messages" over a single connection and
    to be able to separate the messages. We do this by encoding the messages
    in an alphabet that does not contain spaces, then adding a space for
    separation. The last framed message is also followed by a space.
    """

    connection: _IPCHandle

    def __init__(self, name: str, timeout: float | None) -> None:
        self.name = name
        self.timeout = timeout
        self.buffer = bytearray()

    def frame_from_buffer(self) -> bytearray | None:
        """Return a full frame from the bytes we have in the buffer."""
        space_pos = self.buffer.find(b" ")
        if space_pos == -1:
            return None
        # We have a full frame
        bdata = self.buffer[:space_pos]
        self.buffer = self.buffer[space_pos + 1 :]
        return bdata

    def read(self, size: int = 100000) -> str:
        """Read bytes from an IPC connection until we have a full frame."""
        bdata: bytearray | None = bytearray()
        if sys.platform == "win32":
            while True:
                # Check if we already have a message in the buffer before
                # receiving any more data from the socket.
                bdata = self.frame_from_buffer()
                if bdata is not None:
                    break

                # Receive more data into the buffer.
                ov, err = _winapi.ReadFile(self.connection, size, overlapped=True)
                try:
                    if err == _winapi.ERROR_IO_PENDING:
                        timeout = int(self.timeout * 1000) if self.timeout else _winapi.INFINITE
                        res = _winapi.WaitForSingleObject(ov.event, timeout)
                        if res != _winapi.WAIT_OBJECT_0:
                            raise IPCException(f"Bad result from I/O wait: {res}")
                except BaseException:
                    ov.cancel()
                    raise
                _, err = ov.GetOverlappedResult(True)
                more = ov.getbuffer()
                if more:
                    self.buffer.extend(more)
                    bdata = self.frame_from_buffer()
                    if bdata is not None:
                        break
                if err == 0:
                    # we are done!
                    break
                elif err == _winapi.ERROR_MORE_DATA:
                    # read again
                    continue
                elif err == _winapi.ERROR_OPERATION_ABORTED:
                    raise IPCException("ReadFile operation aborted.")
        else:
            while True:
                # Check if we already have a message in the buffer before
                # receiving any more data from the socket.
                bdata = self.frame_from_buffer()
                if bdata is not None:
                    break

                # Receive more data into the buffer.
                more = self.connection.recv(size)
                if not more:
                    # Connection closed
                    break
                self.buffer.extend(more)

        if not bdata:
            # Socket was empty and we didn't get any frame.
            # This should only happen if the socket was closed.
            return ""
        return codecs.decode(bdata, "base64").decode("utf8")

    def write(self, data: str) -> None:
        """Write to an IPC connection."""

        # Frame the data by urlencoding it and separating by space.
        encoded_data = codecs.encode(data.encode("utf8"), "base64") + b" "

        if sys.platform == "win32":
            try:
                ov, err = _winapi.WriteFile(self.connection, encoded_data, overlapped=True)
                try:
                    if err == _winapi.ERROR_IO_PENDING:
                        timeout = int(self.timeout * 1000) if self.timeout else _winapi.INFINITE
                        res = _winapi.WaitForSingleObject(ov.event, timeout)
                        if res != _winapi.WAIT_OBJECT_0:
                            raise IPCException(f"Bad result from I/O wait: {res}")
                    elif err != 0:
                        raise IPCException(f"Failed writing to pipe with error: {err}")
                except BaseException:
                    ov.cancel()
                    raise
                bytes_written, err = ov.GetOverlappedResult(True)
                assert err == 0, err
                assert bytes_written == len(encoded_data)
            except OSError as e:
                raise IPCException(f"Failed to write with error: {e.winerror}") from e
        else:
            self.connection.sendall(encoded_data)

    def close(self) -> None:
        if sys.platform == "win32":
            if self.connection != _winapi.NULL:
                _winapi.CloseHandle(self.connection)
        else:
            self.connection.close()

class IPCClient(IPCBase):
    """The client side of an IPC connection."""

    def __init__(self, name: str, timeout: float | None) -> None:
        super().__init__(name, timeout)
        if sys.platform == "win32":
            timeout = int(self.timeout * 1000) if self.timeout else _winapi.NMPWAIT_WAIT_FOREVER
            try:
                _winapi.WaitNamedPipe(self.name, timeout)
            except FileNotFoundError as e:
                raise IPCException(f"The NamedPipe at {self.name} was not found.") from e
            except OSError as e:
                if e.winerror == _winapi.ERROR_SEM_TIMEOUT:
                    raise IPCException("Timed out waiting for connection.") from e
                else:
                    raise
            try:
                self.connection = _winapi.CreateFile(
                    self.name,
                    _winapi.GENERIC_READ | _winapi.GENERIC_WRITE,
                    0,
                    _winapi.NULL,
                    _winapi.OPEN_EXISTING,
                    _winapi.FILE_FLAG_OVERLAPPED,
                    _winapi.NULL,
                )
            except OSError as e:
                if e.winerror == _winapi.ERROR_PIPE_BUSY:
                    raise IPCException("The connection is busy.") from e
                else:
                    raise
            _winapi.SetNamedPipeHandleState(
                self.connection, _winapi.PIPE_READMODE_MESSAGE, None, None
            )
        else:
            self.connection = socket.socket(socket.AF_UNIX)
            self.connection.settimeout(timeout)
            self.connection.connect(name)

    def __enter__(self) -> IPCClient:
        return self

    def __exit__(
        self,
        exc_ty: type[BaseException] | None = None,
        exc_val: BaseException | None = None,
        exc_tb: TracebackType | None = None,
    ) -> None:
        self.close()

class IPCServer(IPCBase):
    BUFFER_SIZE: Final = 2**16

    def __init__(self, name: str, timeout: float | None = None) -> None:
        if sys.platform == "win32":
            name = r"\\.\pipe\{}-{}.pipe".format(
                name, base64.urlsafe_b64encode(os.urandom(6)).decode()
            )
        else:
            name = f"{name}.sock"
        super().__init__(name, timeout)
        if sys.platform == "win32":
            self.connection = _winapi.CreateNamedPipe(
                self.name,
                _winapi.PIPE_ACCESS_DUPLEX
                | _winapi.FILE_FLAG_FIRST_PIPE_INSTANCE
                | _winapi.FILE_FLAG_OVERLAPPED,
                _winapi.PIPE_READMODE_MESSAGE
                | _winapi.PIPE_TYPE_MESSAGE
                | _winapi.PIPE_WAIT
                | 0x8,  # PIPE_REJECT_REMOTE_CLIENTS
                1,  # one instance
                self.BUFFER_SIZE,
                self.BUFFER_SIZE,
                _winapi.NMPWAIT_WAIT_FOREVER,
                0,  # Use default security descriptor
            )
            if self.connection == -1:  # INVALID_HANDLE_VALUE
                err = _winapi.GetLastError()
                raise IPCException(f"Invalid handle to pipe: {err}")
        else:
            self.sock_directory = tempfile.mkdtemp()
            sockfile = os.path.join(self.sock_directory, self.name)
            self.sock = socket.socket(socket.AF_UNIX)
            self.sock.bind(sockfile)
            self.sock.listen(1)
            if timeout is not None:
                self.sock.settimeout(timeout)

    def __enter__(self) -> IPCServer:
        if sys.platform == "win32":
            # NOTE: It is theoretically possible that this will hang forever if the
            # client never connects, though this can be "solved" by killing the server
            try:
                ov = _winapi.ConnectNamedPipe(self.connection, overlapped=True)
            except OSError as e:
                # Don't raise if the client already exists, or the client already connected
                if e.winerror not in (_winapi.ERROR_PIPE_CONNECTED, _winapi.ERROR_NO_DATA):
                    raise
            else:
                try:
                    timeout = int(self.timeout * 1000) if self.timeout else _winapi.INFINITE
                    res = _winapi.WaitForSingleObject(ov.event, timeout)
                    assert res == _winapi.WAIT_OBJECT_0
                except BaseException:
                    ov.cancel()
                    _winapi.CloseHandle(self.connection)
                    raise
                _, err = ov.GetOverlappedResult(True)
                assert err == 0
        else:
            try:
                self.connection, _ = self.sock.accept()
            except socket.timeout as e:
                raise IPCException("The socket timed out") from e
        return self

    def __exit__(
        self,
        exc_ty: type[BaseException] | None = None,
        exc_val: BaseException | None = None,
        exc_tb: TracebackType | None = None,
    ) -> None:
        if sys.platform == "win32":
            try:
                # Wait for the client to finish reading the last write before disconnecting
                if not FlushFileBuffers(self.connection):
                    raise IPCException(
                        "Failed to flush NamedPipe buffer, maybe the client hung up?"
                    )
            finally:
                DisconnectNamedPipe(self.connection)
        else:
            self.close()

    def cleanup(self) -> None:
        if sys.platform == "win32":
            self.close()
        else:
            shutil.rmtree(self.sock_directory)

    @property
    def connection_name(self) -> str:
        if sys.platform == "win32":
            return self.name
        else:
            name = self.sock.getsockname()
            assert isinstance(name, str)
            return name