"""Client for mypy daemon mode.

This manages a daemon process which keeps useful state in memory
rather than having to read it back from disk on each run.
"""

from __future__ import annotations

import argparse
import base64
import json
import os
import pickle
import sys
import time
import traceback
from typing import Any, Callable, Mapping, NoReturn

from mypy.dmypy_os import alive, kill
from mypy.dmypy_util import DEFAULT_STATUS_FILE, receive, send
from mypy.ipc import IPCClient, IPCException
from mypy.util import check_python_version, get_terminal_width, should_force_color
from mypy.version import __version__

# Argument parser.  Subparsers are tied to action functions by the
# @action(subparse) decorator.

class AugmentedHelpFormatter(argparse.RawDescriptionHelpFormatter):
    def __init__(self, prog: str) -> None:
        super().__init__(prog=prog, max_help_position=30)

parser = argparse.ArgumentParser(
    prog="dmypy", description="Client for mypy daemon mode", fromfile_prefix_chars="@"
)
parser.set_defaults(action=None)
parser.add_argument(
    "--status-file", default=DEFAULT_STATUS_FILE, help="status file to retrieve daemon details"
)
parser.add_argument(
    "-V",
    "--version",
    action="version",
    version="%(prog)s " + __version__,
    help="Show program's version number and exit",
)
subparsers = parser.add_subparsers()

start_parser = p = subparsers.add_parser("start", help="Start daemon")
p.add_argument("--log-file", metavar="FILE", type=str, help="Direct daemon stdout/stderr to FILE")
p.add_argument(
    "--timeout", metavar="TIMEOUT", type=int, help="Server shutdown timeout (in seconds)"
)
p.add_argument(
    "flags", metavar="FLAG", nargs="*", type=str, help="Regular mypy flags (precede with --)"
)

restart_parser = p = subparsers.add_parser(
    "restart", help="Restart daemon (stop or kill followed by start)"
)
p.add_argument("--log-file", metavar="FILE", type=str, help="Direct daemon stdout/stderr to FILE")
p.add_argument(
    "--timeout", metavar="TIMEOUT", type=int, help="Server shutdown timeout (in seconds)"
)
p.add_argument(
    "flags", metavar="FLAG", nargs="*", type=str, help="Regular mypy flags (precede with --)"
)

status_parser = p = subparsers.add_parser("status", help="Show daemon status")
p.add_argument("-v", "--verbose", action="store_true", help="Print detailed status")
p.add_argument("--fswatcher-dump-file", help="Collect information about the current file state")

stop_parser = p = subparsers.add_parser("stop", help="Stop daemon (asks it politely to go away)")

kill_parser = p = subparsers.add_parser("kill", help="Kill daemon (kills the process)")

check_parser = p = subparsers.add_parser(
    "check", formatter_class=AugmentedHelpFormatter, help="Check some files (requires daemon)"
)
p.add_argument("-v", "--verbose", action="store_true", help="Print detailed status")
p.add_argument("-q", "--quiet", action="store_true", help=argparse.SUPPRESS)  # Deprecated
p.add_argument("--junit-xml", help="Write junit.xml to the given file")
p.add_argument("--perf-stats-file", help="write performance information to the given file")
p.add_argument("files", metavar="FILE", nargs="+", help="File (or directory) to check")
p.add_argument(
    "--export-types",
    action="store_true",
    help="Store types of all expressions in a shared location (useful for inspections)",
)

run_parser = p = subparsers.add_parser(
    "run",
    formatter_class=AugmentedHelpFormatter,
    help="Check some files, [re]starting daemon if necessary",
)
p.add_argument("-v", "--verbose", action="store_true", help="Print detailed status")
p.add_argument("--junit-xml", help="Write junit.xml to the given file")
p.add_argument("--perf-stats-file", help="write performance information to the given file")
p.add_argument(
    "--timeout", metavar="TIMEOUT", type=int, help="Server shutdown timeout (in seconds)"
)
p.add_argument("--log-file", metavar="FILE", type=str, help="Direct daemon stdout/stderr to FILE")
p.add_argument(
    "--export-types",
    action="store_true",
    help="Store types of all expressions in a shared location (useful for inspections)",
)
p.add_argument(
    "flags",
    metavar="ARG",
    nargs="*",
    type=str,
    help="Regular mypy flags and files (precede with --)",
)

recheck_parser = p = subparsers.add_parser(
    "recheck",
    formatter_class=AugmentedHelpFormatter,
    help="Re-check the previous list of files, with optional modifications (requires daemon)",
)
p.add_argument("-v", "--verbose", action="store_true", help="Print detailed status")
p.add_argument("-q", "--quiet", action="store_true", help=argparse.SUPPRESS)  # Deprecated
p.add_argument("--junit-xml", help="Write junit.xml to the given file")
p.add_argument("--perf-stats-file", help="write performance information to the given file")
p.add_argument(
    "--export-types",
    action="store_true",
    help="Store types of all expressions in a shared location (useful for inspections)",
)
p.add_argument(
    "--update",
    metavar="FILE",
    nargs="*",
    help="Files in the run to add or check again (default: all from previous run)",
)
p.add_argument("--remove", metavar="FILE", nargs="*", help="Files to remove from the run")

suggest_parser = p = subparsers.add_parser(
    "suggest", help="Suggest a signature or show call sites for a specific function"
)
p.add_argument(
    "function",
    metavar="FUNCTION",
    type=str,
    help="Function specified as '[package.]module.[class.]function'",
)
p.add_argument(
    "--json",
    action="store_true",
    help="Produce json that pyannotate can use to apply a suggestion",
)
p.add_argument(
    "--no-errors", action="store_true", help="Only produce suggestions that cause no errors"
)
p.add_argument(
    "--no-any", action="store_true", help="Only produce suggestions that don't contain Any"
)
p.add_argument(
    "--flex-any",
    type=float,
    help="Allow anys in types if they go above a certain score (scores are from 0-1)",
)
p.add_argument(
    "--callsites", action="store_true", help="Find callsites instead of suggesting a type"
)
p.add_argument(
    "--use-fixme",
    metavar="NAME",
    type=str,
    help="A dummy name to use instead of Any for types that can't be inferred",
)
p.add_argument(
    "--max-guesses",
    type=int,
    help="Set the maximum number of types to try for a function (default 64)",
)

inspect_parser = p = subparsers.add_parser(
    "inspect", help="Locate and statically inspect expression(s)"
)
p.add_argument(
    "location",
    metavar="LOCATION",
    type=str,
    help="Location specified as path/to/file.py:line:column[:end_line:end_column]."
    " If position is given (i.e. only line and column), this will return all"
    " enclosing expressions",
)
p.add_argument(
    "--show",
    metavar="INSPECTION",
    type=str,
    default="type",
    choices=["type", "attrs", "definition"],
    help="What kind of inspection to run",
)
p.add_argument(
    "--verbose",
    "-v",
    action="count",
    default=0,
    help="Increase verbosity of the type string representation (can be repeated)",
)
p.add_argument(
    "--limit",
    metavar="NUM",
    type=int,
    default=0,
    help="Return at most NUM innermost expressions (if position is given); 0 means no limit",
)
p.add_argument(
    "--include-span",
    action="store_true",
    help="Prepend each inspection result with the span of corresponding expression"
    ' (e.g. 1:2:3:4:"int")',
)
p.add_argument(
    "--include-kind",
    action="store_true",
    help="Prepend each inspection result with the kind of corresponding expression"
    ' (e.g. NameExpr:"int")',
)
p.add_argument(
    "--include-object-attrs",
    action="store_true",
    help='Include attributes of "object" in "attrs" inspection',
)
p.add_argument(
    "--union-attrs",
    action="store_true",
    help="Include attributes valid for some of possible expression types"
    " (by default an intersection is returned)",
)
p.add_argument(
    "--force-reload",
    action="store_true",
    help="Re-parse and re-type-check file before inspection (may be slow)",
)

hang_parser = p = subparsers.add_parser("hang", help="Hang for 100 seconds")

daemon_parser = p = subparsers.add_parser("daemon", help="Run daemon in foreground")
p.add_argument(
    "--timeout", metavar="TIMEOUT", type=int, help="Server shutdown timeout (in seconds)"
)
p.add_argument("--log-file", metavar="FILE", type=str, help="Direct daemon stdout/stderr to FILE")
p.add_argument(
    "flags", metavar="FLAG", nargs="*", type=str, help="Regular mypy flags (precede with --)"
)
p.add_argument("--options-data", help=argparse.SUPPRESS)
help_parser = p = subparsers.add_parser("help")

del p

class BadStatus(Exception):
    """Exception raised when there is something wrong with the status file.

    For example:
    - No status file found
    - Status file malformed
    - Process whose pid is in the status file does not exist
    """

def main(argv: list[str]) -> None:
    """The code is top-down."""
    check_python_version("dmypy")
    args = parser.parse_args(argv)
    if not args.action:
        parser.print_usage()
    else:
        try:
            args.action(args)
        except BadStatus as err:
            fail(err.args[0])
        except Exception:
            # We do this explicitly to avoid exceptions percolating up
            # through mypy.api invocations
            traceback.print_exc()
            sys.exit(2)

def fail(msg: str) -> NoReturn:
    print(msg, file=sys.stderr)
    sys.exit(2)

ActionFunction = Callable[[argparse.Namespace], None]

def action(subparser: argparse.ArgumentParser) -> Callable[[ActionFunction], ActionFunction]:
    """Decorator to tie an action function to a subparser."""

    def register(func: ActionFunction) -> ActionFunction:
        subparser.set_defaults(action=func)
        return func

    return register

# Action functions (run in client from command line).

@action(start_parser)
def do_start(args: argparse.Namespace) -> None:
    """Start daemon (it must not already be running).

    This is where mypy flags are set from the command line.

    Setting flags is a bit awkward; you have to use e.g.:

      dmypy start -- --strict

    since we don't want to duplicate mypy's huge list of flags.
    """
    try:
        get_status(args.status_file)
    except BadStatus:
        # Bad or missing status file or dead process; good to start.
        pass
    else:
        fail("Daemon is still alive")
    start_server(args)

@action(restart_parser)
def do_restart(args: argparse.Namespace) -> None:
    """Restart daemon (it may or may not be running; but not hanging).

    We first try to stop it politely if it's running.  This also sets
    mypy flags from the command line (see do_start()).
    """
    restart_server(args)

def restart_server(args: argparse.Namespace, allow_sources: bool = False) -> None:
    """Restart daemon (it may or may not be running; but not hanging)."""
    try:
        do_stop(args)
    except BadStatus:
        # Bad or missing status file or dead process; good to start.
        pass
    start_server(args, allow_sources)

def start_server(args: argparse.Namespace, allow_sources: bool = False) -> None:
    """Start the server from command arguments and wait for it."""
    # Lazy import so this import doesn't slow down other commands.
    from mypy.dmypy_server import daemonize, process_start_options

    start_options = process_start_options(args.flags, allow_sources)
    if daemonize(start_options, args.status_file, timeout=args.timeout, log_file=args.log_file):
        sys.exit(2)
    wait_for_server(args.status_file)

def wait_for_server(status_file: str, timeout: float = 5.0) -> None:
    """Wait until the server is up.

    Exit if it doesn't happen within the timeout.
    """
    endtime = time.time() + timeout
    while time.time() < endtime:
        try:
            data = read_status(status_file)
        except BadStatus:
            # If the file isn't there yet, retry later.
            time.sleep(0.1)
            continue
        # If the file's content is bogus or the process is dead, fail.
        check_status(data)
        print("Daemon started")
        return
    fail("Timed out waiting for daemon to start")

@action(run_parser)
def do_run(args: argparse.Namespace) -> None:
    """Do a check, starting (or restarting) the daemon as necessary

    Restarts the daemon if the running daemon reports that it is
    required (due to a configuration change, for example).

    Setting flags is a bit awkward; you have to use e.g.:

      dmypy run -- --strict a.py b.py ...

    since we don't want to duplicate mypy's huge list of flags.
    (The -- is only necessary if flags are specified.)
    """
    if not is_running(args.status_file):
        # Bad or missing status file or dead process; good to start.
        start_server(args, allow_sources=True)
    t0 = time.time()
    response = request(
        args.status_file,
        "run",
        version=__version__,
        args=args.flags,
        export_types=args.export_types,
    )
    # If the daemon signals that a restart is necessary, do it
    if "restart" in response:
        print(f"Restarting: {response['restart']}")
        restart_server(args, allow_sources=True)
        response = request(
            args.status_file,
            "run",
            version=__version__,
            args=args.flags,
            export_types=args.export_types,
        )

    t1 = time.time()
    response["roundtrip_time"] = t1 - t0
    check_output(response, args.verbose, args.junit_xml, args.perf_stats_file)

@action(status_parser)
def do_status(args: argparse.Namespace) -> None:
    """Print daemon status.

    This verifies that it is responsive to requests.
    """
    status = read_status(args.status_file)
    if args.verbose:
        show_stats(status)
    # Both check_status() and request() may raise BadStatus,
    # which will be handled by main().
    check_status(status)
    response = request(
        args.status_file, "status", fswatcher_dump_file=args.fswatcher_dump_file, timeout=5
    )
    if args.verbose or "error" in response:
        show_stats(response)
    if "error" in response:
        fail(f"Daemon is stuck; consider {sys.argv[0]} kill")
    print("Daemon is up and running")

@action(stop_parser)
def do_stop(args: argparse.Namespace) -> None:
    """Stop daemon via a 'stop' request."""
    # May raise BadStatus, which will be handled by main().
    response = request(args.status_file, "stop", timeout=5)
    if "error" in response:
        show_stats(response)
        fail(f"Daemon is stuck; consider {sys.argv[0]} kill")
    else:
        print("Daemon stopped")

@action(kill_parser)
def do_kill(args: argparse.Namespace) -> None:
    """Kill daemon process with SIGKILL."""
    pid, _ = get_status(args.status_file)
    try:
        kill(pid)
    except OSError as err:
        fail(str(err))
    else:
        print("Daemon killed")

@action(check_parser)
def do_check(args: argparse.Namespace) -> None:
    """Ask the daemon to check a list of files."""
    t0 = time.time()
    response = request(args.status_file, "check", files=args.files, export_types=args.export_types)
    t1 = time.time()
    response["roundtrip_time"] = t1 - t0
    check_output(response, args.verbose, args.junit_xml, args.perf_stats_file)

@action(recheck_parser)
def do_recheck(args: argparse.Namespace) -> None:
    """Ask the daemon to recheck the previous list of files, with optional modifications.

    If at least one of --remove or --update is given, the server will
    update the list of files to check accordingly and assume that any other files
    are unchanged.  If none of these flags are given, the server will call stat()
    on each file last checked to determine its status.

    Files given in --update ought to exist.  Files given in --remove need not exist;
    if they don't they will be ignored.
    The lists may be empty but oughtn't contain duplicates or overlap.

    NOTE: The list of files is lost when the daemon is restarted.
    """
    t0 = time.time()
    if args.remove is not None or args.update is not None:
        response = request(
            args.status_file,
            "recheck",
            export_types=args.export_types,
            remove=args.remove,
            update=args.update,
        )
    else:
        response = request(args.status_file, "recheck", export_types=args.export_types)
    t1 = time.time()
    response["roundtrip_time"] = t1 - t0
    check_output(response, args.verbose, args.junit_xml, args.perf_stats_file)

@action(suggest_parser)
def do_suggest(args: argparse.Namespace) -> None:
    """Ask the daemon for a suggested signature.

    This just prints whatever the daemon reports as output.
    For now it may be closer to a list of call sites.
    """
    response = request(
        args.status_file,
        "suggest",
        function=args.function,
        json=args.json,
        callsites=args.callsites,
        no_errors=args.no_errors,
        no_any=args.no_any,
        flex_any=args.flex_any,
        use_fixme=args.use_fixme,
        max_guesses=args.max_guesses,
    )
    check_output(response, verbose=False, junit_xml=None, perf_stats_file=None)

@action(inspect_parser)
def do_inspect(args: argparse.Namespace) -> None:
    """Ask daemon to print the type of an expression."""
    response = request(
        args.status_file,
        "inspect",
        show=args.show,
        location=args.location,
        verbosity=args.verbose,
        limit=args.limit,
        include_span=args.include_span,
        include_kind=args.include_kind,
        include_object_attrs=args.include_object_attrs,
        union_attrs=args.union_attrs,
        force_reload=args.force_reload,
    )
    check_output(response, verbose=False, junit_xml=None, perf_stats_file=None)

def check_output(
    response: dict[str, Any], verbose: bool, junit_xml: str | None, perf_stats_file: str | None
) -> None:
    """Print the output from a check or recheck command.

    Call sys.exit() unless the status code is zero.
    """
    if "error" in response:
        fail(response["error"])
    try:
        out, err, status_code = response["out"], response["err"], response["status"]
    except KeyError:
        fail(f"Response: {str(response)}")
    sys.stdout.write(out)
    sys.stdout.flush()
    sys.stderr.write(err)
    sys.stderr.flush()
    if verbose:
        show_stats(response)
    if junit_xml:
        # Lazy import so this import doesn't slow things down when not writing junit
        from mypy.util import write_junit_xml

        messages = (out + err).splitlines()
        write_junit_xml(
            response["roundtrip_time"],
            bool(err),
            {None: messages} if messages else {},
            junit_xml,
            response["python_version"],
            response["platform"],
        )
    if perf_stats_file:
        telemetry = response.get("stats", {})
        with open(perf_stats_file, "w") as f:
            json.dump(telemetry, f)

    if status_code:
        sys.exit(status_code)

def show_stats(response: Mapping[str, object]) -> None:
    for key, value in sorted(response.items()):
        if key in ("out", "err", "stdout", "stderr"):
            # Special case text output to display just 40 characters of text
            value = repr(value)[1:-1]
            if len(value) > 50:
                value = f"{value[:40]} ... {len(value)-40} more characters"
            print("%-24s: %s" % (key, value))
            continue
        print("%-24s: %10s" % (key, "%.3f" % value if isinstance(value, float) else value))

@action(hang_parser)
def do_hang(args: argparse.Namespace) -> None:
    """Hang for 100 seconds, as a debug hack."""
    print(request(args.status_file, "hang", timeout=1))

@action(daemon_parser)
def do_daemon(args: argparse.Namespace) -> None:
    """Serve requests in the foreground."""
    # Lazy import so this import doesn't slow down other commands.
    from mypy.dmypy_server import Server, process_start_options

    if args.log_file:
        sys.stdout = sys.stderr = open(args.log_file, "a", buffering=1)
        fd = sys.stdout.fileno()
        os.dup2(fd, 2)
        os.dup2(fd, 1)

    if args.options_data:
        from mypy.options import Options

        options_dict = pickle.loads(base64.b64decode(args.options_data))
        options_obj = Options()
        options = options_obj.apply_changes(options_dict)
    else:
        options = process_start_options(args.flags, allow_sources=False)

    Server(options, args.status_file, timeout=args.timeout).serve()

@action(help_parser)
def do_help(args: argparse.Namespace) -> None:
    """Print full help (same as dmypy --help)."""
    parser.print_help()

# Client-side infrastructure.

def request(
    status_file: str, command: str, *, timeout: int | None = None, **kwds: object
) -> dict[str, Any]:
    """Send a request to the daemon.

    Return the JSON dict with the response.

    Raise BadStatus if there is something wrong with the status file
    or if the process whose pid is in the status file has died.

    Return {'error': <message>} if an IPC operation or receive()
    raised OSError.  This covers cases such as connection refused or
    closed prematurely as well as invalid JSON received.
    """
    response: dict[str, str] = {}
    args = dict(kwds)
    args["command"] = command
    # Tell the server whether this request was initiated from a human-facing terminal,
    # so that it can format the type checking output accordingly.
    args["is_tty"] = sys.stdout.isatty() or should_force_color()
    args["terminal_width"] = get_terminal_width()
    _, name = get_status(status_file)
    try:
        with IPCClient(name, timeout) as client:
            send(client, args)

            final = False
            while not final:
                response = receive(client)
                final = bool(response.pop("final", False))
                # Display debugging output written to stdout/stderr in the server process for convenience.
                # This should not be confused with "out" and "err" fields in the response.
                # Those fields hold the output of the "check" command, and are handled in check_output().
                stdout = response.pop("stdout", None)
                if stdout:
                    sys.stdout.write(stdout)
                stderr = response.pop("stderr", None)
                if stderr:
                    sys.stderr.write(stderr)
    except (OSError, IPCException) as err:
        return {"error": str(err)}
    # TODO: Other errors, e.g. ValueError, UnicodeError

    return response

def get_status(status_file: str) -> tuple[int, str]:
    """Read status file and check if the process is alive.

    Return (pid, connection_name) on success.

    Raise BadStatus if something's wrong.
    """
    data = read_status(status_file)
    return check_status(data)

def check_status(data: dict[str, Any]) -> tuple[int, str]:
    """Check if the process is alive.

    Return (pid, connection_name) on success.

    Raise BadStatus if something's wrong.
    """
    if "pid" not in data:
        raise BadStatus("Invalid status file (no pid field)")
    pid = data["pid"]
    if not isinstance(pid, int):
        raise BadStatus("pid field is not an int")
    if not alive(pid):
        raise BadStatus("Daemon has died")
    if "connection_name" not in data:
        raise BadStatus("Invalid status file (no connection_name field)")
    connection_name = data["connection_name"]
    if not isinstance(connection_name, str):
        raise BadStatus("connection_name field is not a string")
    return pid, connection_name

def read_status(status_file: str) -> dict[str, object]:
    """Read status file.

    Raise BadStatus if the status file doesn't exist or contains
    invalid JSON or the JSON is not a dict.
    """
    if not os.path.isfile(status_file):
        raise BadStatus("No status file found")
    with open(status_file) as f:
        try:
            data = json.load(f)
        except Exception as e:
            raise BadStatus("Malformed status file (not JSON)") from e
    if not isinstance(data, dict):
        raise BadStatus("Invalid status file (not a dict)")
    return data

def is_running(status_file: str) -> bool:
    """Check if the server is running cleanly"""
    try:
        get_status(status_file)
    except BadStatus:
        return False
    return True

# Run main().
def console_entry() -> None:
    main(sys.argv[1:])


from __future__ import annotations

import sys
from typing import Any, Callable

if sys.platform == "win32":
    import ctypes
    import subprocess
    from ctypes.wintypes import DWORD, HANDLE

    PROCESS_QUERY_LIMITED_INFORMATION = ctypes.c_ulong(0x1000)

    kernel32 = ctypes.windll.kernel32
    OpenProcess: Callable[[DWORD, int, int], HANDLE] = kernel32.OpenProcess
    GetExitCodeProcess: Callable[[HANDLE, Any], int] = kernel32.GetExitCodeProcess
else:
    import os
    import signal

def alive(pid: int) -> bool:
    """Is the process alive?"""
    if sys.platform == "win32":
        # why can't anything be easy...
        status = DWORD()
        handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
        GetExitCodeProcess(handle, ctypes.byref(status))
        return status.value == 259  # STILL_ACTIVE
    else:
        try:
            os.kill(pid, 0)
        except OSError:
            return False
        return True

def kill(pid: int) -> None:
    """Kill the process."""
    if sys.platform == "win32":
        subprocess.check_output(f"taskkill /pid {pid} /f /t")
    else:
        os.kill(pid, signal.SIGKILL)


"""Server for mypy daemon mode.

This implements a daemon process which keeps useful state in memory
to enable fine-grained incremental reprocessing of changes.
"""

from __future__ import annotations

import argparse
import base64
import io
import json
import os
import pickle
import subprocess
import sys
import time
import traceback
from contextlib import redirect_stderr, redirect_stdout
from typing import AbstractSet, Any, Callable, Final, List, Sequence, Tuple
from typing_extensions import TypeAlias as _TypeAlias

import mypy.build
import mypy.errors
import mypy.main
from mypy.dmypy_util import WriteToConn, receive, send
from mypy.find_sources import InvalidSourceList, create_source_list
from mypy.fscache import FileSystemCache
from mypy.fswatcher import FileData, FileSystemWatcher
from mypy.inspections import InspectionEngine
from mypy.ipc import IPCServer
from mypy.modulefinder import BuildSource, FindModuleCache, SearchPaths, compute_search_paths
from mypy.options import Options
from mypy.server.update import FineGrainedBuildManager, refresh_suppressed_submodules
from mypy.suggestions import SuggestionEngine, SuggestionFailure
from mypy.typestate import reset_global_state
from mypy.util import FancyFormatter, count_stats
from mypy.version import __version__

MEM_PROFILE: Final = False  # If True, dump memory profile after initialization

if sys.platform == "win32":
    from subprocess import STARTUPINFO

    def daemonize(
        options: Options, status_file: str, timeout: int | None = None, log_file: str | None = None
    ) -> int:
        """Create the daemon process via "dmypy daemon" and pass options via command line

        When creating the daemon grandchild, we create it in a new console, which is
        started hidden. We cannot use DETACHED_PROCESS since it will cause console windows
        to pop up when starting. See
        https://github.com/python/cpython/pull/4150#issuecomment-340215696
        for more on why we can't have nice things.

        It also pickles the options to be unpickled by mypy.
        """
        command = [sys.executable, "-m", "mypy.dmypy", "--status-file", status_file, "daemon"]
        pickled_options = pickle.dumps(options.snapshot())
        command.append(f'--options-data="{base64.b64encode(pickled_options).decode()}"')
        if timeout:
            command.append(f"--timeout={timeout}")
        if log_file:
            command.append(f"--log-file={log_file}")
        info = STARTUPINFO()
        info.dwFlags = 0x1  # STARTF_USESHOWWINDOW aka use wShowWindow's value
        info.wShowWindow = 0  # SW_HIDE aka make the window invisible
        try:
            subprocess.Popen(command, creationflags=0x10, startupinfo=info)  # CREATE_NEW_CONSOLE
            return 0
        except subprocess.CalledProcessError as e:
            return e.returncode

else:

    def _daemonize_cb(func: Callable[[], None], log_file: str | None = None) -> int:
        """Arrange to call func() in a grandchild of the current process.

        Return 0 for success, exit status for failure, negative if
        subprocess killed by signal.
        """
        # See https://stackoverflow.com/questions/473620/how-do-you-create-a-daemon-in-python
        sys.stdout.flush()
        sys.stderr.flush()
        pid = os.fork()
        if pid:
            # Parent process: wait for child in case things go bad there.
            npid, sts = os.waitpid(pid, 0)
            sig = sts & 0xFF
            if sig:
                print("Child killed by signal", sig)
                return -sig
            sts = sts >> 8
            if sts:
                print("Child exit status", sts)
            return sts
        # Child process: do a bunch of UNIX stuff and then fork a grandchild.
        try:
            os.setsid()  # Detach controlling terminal
            os.umask(0o27)
            devnull = os.open("/dev/null", os.O_RDWR)
            os.dup2(devnull, 0)
            os.dup2(devnull, 1)
            os.dup2(devnull, 2)
            os.close(devnull)
            pid = os.fork()
            if pid:
                # Child is done, exit to parent.
                os._exit(0)
            # Grandchild: run the server.
            if log_file:
                sys.stdout = sys.stderr = open(log_file, "a", buffering=1)
                fd = sys.stdout.fileno()
                os.dup2(fd, 2)
                os.dup2(fd, 1)
            func()
        finally:
            # Make sure we never get back into the caller.
            os._exit(1)

    def daemonize(
        options: Options, status_file: str, timeout: int | None = None, log_file: str | None = None
    ) -> int:
        """Run the mypy daemon in a grandchild of the current process

        Return 0 for success, exit status for failure, negative if
        subprocess killed by signal.
        """
        return _daemonize_cb(Server(options, status_file, timeout).serve, log_file)

# Server code.

CONNECTION_NAME: Final = "dmypy"

def process_start_options(flags: list[str], allow_sources: bool) -> Options:
    _, options = mypy.main.process_options(
        ["-i"] + flags, require_targets=False, server_options=True
    )
    if options.report_dirs:
        print("dmypy: Ignoring report generation settings. Start/restart cannot generate reports.")
    if options.junit_xml:
        print(
            "dmypy: Ignoring report generation settings. "
            "Start/restart does not support --junit-xml. Pass it to check/recheck instead"
        )
        options.junit_xml = None
    if not options.incremental:
        sys.exit("dmypy: start/restart should not disable incremental mode")
    if options.follow_imports not in ("skip", "error", "normal"):
        sys.exit("dmypy: follow-imports=silent not supported")
    return options

def ignore_suppressed_imports(module: str) -> bool:
    """Can we skip looking for newly unsuppressed imports to module?"""
    # Various submodules of 'encodings' can be suppressed, since it
    # uses module-level '__getattr__'. Skip them since there are many
    # of them, and following imports to them is kind of pointless.
    return module.startswith("encodings.")

ModulePathPair: _TypeAlias = Tuple[str, str]
ModulePathPairs: _TypeAlias = List[ModulePathPair]
ChangesAndRemovals: _TypeAlias = Tuple[ModulePathPairs, ModulePathPairs]

class Server:
    # NOTE: the instance is constructed in the parent process but
    # serve() is called in the grandchild (by daemonize()).

    def __init__(self, options: Options, status_file: str, timeout: int | None = None) -> None:
        """Initialize the server with the desired mypy flags."""
        self.options = options
        # Snapshot the options info before we muck with it, to detect changes
        self.options_snapshot = options.snapshot()
        self.timeout = timeout
        self.fine_grained_manager: FineGrainedBuildManager | None = None

        if os.path.isfile(status_file):
            os.unlink(status_file)

        self.fscache = FileSystemCache()

        options.raise_exceptions = True
        options.incremental = True
        options.fine_grained_incremental = True
        options.show_traceback = True
        if options.use_fine_grained_cache:
            # Using fine_grained_cache implies generating and caring
            # about the fine grained cache
            options.cache_fine_grained = True
        else:
            options.cache_dir = os.devnull
        # Fine-grained incremental doesn't support general partial types
        # (details in https://github.com/python/mypy/issues/4492)
        options.local_partial_types = True
        self.status_file = status_file

        # Since the object is created in the parent process we can check
        # the output terminal options here.
        self.formatter = FancyFormatter(sys.stdout, sys.stderr, options.hide_error_codes)

    def _response_metadata(self) -> dict[str, str]:
        py_version = f"{self.options.python_version[0]}_{self.options.python_version[1]}"
        return {"platform": self.options.platform, "python_version": py_version}

    def serve(self) -> None:
        """Serve requests, synchronously (no thread or fork)."""

        command = None
        server = IPCServer(CONNECTION_NAME, self.timeout)
        orig_stdout = sys.stdout
        orig_stderr = sys.stderr

        try:
            with open(self.status_file, "w") as f:
                json.dump({"pid": os.getpid(), "connection_name": server.connection_name}, f)
                f.write("\n")  # I like my JSON with a trailing newline
            while True:
                with server:
                    data = receive(server)
                    sys.stdout = WriteToConn(server, "stdout", sys.stdout.isatty())
                    sys.stderr = WriteToConn(server, "stderr", sys.stderr.isatty())
                    resp: dict[str, Any] = {}
                    if "command" not in data:
                        resp = {"error": "No command found in request"}
                    else:
                        command = data["command"]
                        if not isinstance(command, str):
                            resp = {"error": "Command is not a string"}
                        else:
                            command = data.pop("command")
                            try:
                                resp = self.run_command(command, data)
                            except Exception:
                                # If we are crashing, report the crash to the client
                                tb = traceback.format_exception(*sys.exc_info())
                                resp = {"error": "Daemon crashed!\n" + "".join(tb)}
                                resp.update(self._response_metadata())
                                resp["final"] = True
                                send(server, resp)
                                raise
                    resp["final"] = True
                    try:
                        resp.update(self._response_metadata())
                        send(server, resp)
                    except OSError:
                        pass  # Maybe the client hung up
                    if command == "stop":
                        reset_global_state()
                        sys.exit(0)
        finally:
            # Revert stdout/stderr so we can see any errors.
            sys.stdout = orig_stdout
            sys.stderr = orig_stderr

            # If the final command is something other than a clean
            # stop, remove the status file. (We can't just
            # simplify the logic and always remove the file, since
            # that could cause us to remove a future server's
            # status file.)
            if command != "stop":
                os.unlink(self.status_file)
            try:
                server.cleanup()  # try to remove the socket dir on Linux
            except OSError:
                pass
            exc_info = sys.exc_info()
            if exc_info[0] and exc_info[0] is not SystemExit:
                traceback.print_exception(*exc_info)

    def run_command(self, command: str, data: dict[str, object]) -> dict[str, object]:
        """Run a specific command from the registry."""
        key = "cmd_" + command
        method = getattr(self.__class__, key, None)
        if method is None:
            return {"error": f"Unrecognized command '{command}'"}
        else:
            if command not in {"check", "recheck", "run"}:
                # Only the above commands use some error formatting.
                del data["is_tty"]
                del data["terminal_width"]
            ret = method(self, **data)
            assert isinstance(ret, dict)
            return ret

    # Command functions (run in the server via RPC).

    def cmd_status(self, fswatcher_dump_file: str | None = None) -> dict[str, object]:
        """Return daemon status."""
        res: dict[str, object] = {}
        res.update(get_meminfo())
        if fswatcher_dump_file:
            data = self.fswatcher.dump_file_data() if hasattr(self, "fswatcher") else {}
            # Using .dumps and then writing was noticeably faster than using dump
            s = json.dumps(data)
            with open(fswatcher_dump_file, "w") as f:
                f.write(s)
        return res

    def cmd_stop(self) -> dict[str, object]:
        """Stop daemon."""
        # We need to remove the status file *before* we complete the
        # RPC. Otherwise a race condition exists where a subsequent
        # command can see a status file from a dying server and think
        # it is a live one.
        os.unlink(self.status_file)
        return {}

    def cmd_run(
        self,
        version: str,
        args: Sequence[str],
        export_types: bool,
        is_tty: bool,
        terminal_width: int,
    ) -> dict[str, object]:
        """Check a list of files, triggering a restart if needed."""
        stderr = io.StringIO()
        stdout = io.StringIO()
        try:
            # Process options can exit on improper arguments, so we need to catch that and
            # capture stderr so the client can report it
            with redirect_stderr(stderr):
                with redirect_stdout(stdout):
                    sources, options = mypy.main.process_options(
                        ["-i"] + list(args),
                        require_targets=True,
                        server_options=True,
                        fscache=self.fscache,
                        program="mypy-daemon",
                        header=argparse.SUPPRESS,
                    )
            # Signal that we need to restart if the options have changed
            if not options.compare_stable(self.options_snapshot):
                return {"restart": "configuration changed"}
            if __version__ != version:
                return {"restart": "mypy version changed"}
            if self.fine_grained_manager:
                manager = self.fine_grained_manager.manager
                start_plugins_snapshot = manager.plugins_snapshot
                _, current_plugins_snapshot = mypy.build.load_plugins(
                    options, manager.errors, sys.stdout, extra_plugins=()
                )
                if current_plugins_snapshot != start_plugins_snapshot:
                    return {"restart": "plugins changed"}
        except InvalidSourceList as err:
            return {"out": "", "err": str(err), "status": 2}
        except SystemExit as e:
            return {"out": stdout.getvalue(), "err": stderr.getvalue(), "status": e.code}
        return self.check(sources, export_types, is_tty, terminal_width)

    def cmd_check(
        self, files: Sequence[str], export_types: bool, is_tty: bool, terminal_width: int
    ) -> dict[str, object]:
        """Check a list of files."""
        try:
            sources = create_source_list(files, self.options, self.fscache)
        except InvalidSourceList as err:
            return {"out": "", "err": str(err), "status": 2}
        return self.check(sources, export_types, is_tty, terminal_width)

    def cmd_recheck(
        self,
        is_tty: bool,
        terminal_width: int,
        export_types: bool,
        remove: list[str] | None = None,
        update: list[str] | None = None,
    ) -> dict[str, object]:
        """Check the same list of files we checked most recently.

        If remove/update is given, they modify the previous list;
        if all are None, stat() is called for each file in the previous list.
        """
        t0 = time.time()
        if not self.fine_grained_manager:
            return {"error": "Command 'recheck' is only valid after a 'check' command"}
        sources = self.previous_sources
        if remove:
            removals = set(remove)
            sources = [s for s in sources if s.path and s.path not in removals]
        if update:
            # Sort list of file updates by extension, so *.pyi files are first.
            update.sort(key=lambda f: os.path.splitext(f)[1], reverse=True)

            known = {s.path for s in sources if s.path}
            added = [p for p in update if p not in known]
            try:
                added_sources = create_source_list(added, self.options, self.fscache)
            except InvalidSourceList as err:
                return {"out": "", "err": str(err), "status": 2}
            sources = sources + added_sources  # Make a copy!
        t1 = time.time()
        manager = self.fine_grained_manager.manager
        manager.log(f"fine-grained increment: cmd_recheck: {t1 - t0:.3f}s")
        old_export_types = self.options.export_types
        self.options.export_types = self.options.export_types or export_types
        if not self.following_imports():
            messages = self.fine_grained_increment(
                sources, remove, update, explicit_export_types=export_types
            )
        else:
            assert remove is None and update is None
            messages = self.fine_grained_increment_follow_imports(
                sources, explicit_export_types=export_types
            )
        res = self.increment_output(messages, sources, is_tty, terminal_width)
        self.flush_caches()
        self.update_stats(res)
        self.options.export_types = old_export_types
        return res

    def check(
        self, sources: list[BuildSource], export_types: bool, is_tty: bool, terminal_width: int
    ) -> dict[str, Any]:
        """Check using fine-grained incremental mode.

        If is_tty is True format the output nicely with colors and summary line
        (unless disabled in self.options). Also pass the terminal_width to formatter.
        """
        old_export_types = self.options.export_types
        self.options.export_types = self.options.export_types or export_types
        if not self.fine_grained_manager:
            res = self.initialize_fine_grained(sources, is_tty, terminal_width)
        else:
            if not self.following_imports():
                messages = self.fine_grained_increment(sources, explicit_export_types=export_types)
            else:
                messages = self.fine_grained_increment_follow_imports(
                    sources, explicit_export_types=export_types
                )
            res = self.increment_output(messages, sources, is_tty, terminal_width)
        self.flush_caches()
        self.update_stats(res)
        self.options.export_types = old_export_types
        return res

    def flush_caches(self) -> None:
        self.fscache.flush()
        if self.fine_grained_manager:
            self.fine_grained_manager.flush_cache()

    def update_stats(self, res: dict[str, Any]) -> None:
        if self.fine_grained_manager:
            manager = self.fine_grained_manager.manager
            manager.dump_stats()
            res["stats"] = manager.stats
            manager.stats = {}

    def following_imports(self) -> bool:
        """Are we following imports?"""
        # TODO: What about silent?
        return self.options.follow_imports == "normal"

    def initialize_fine_grained(
        self, sources: list[BuildSource], is_tty: bool, terminal_width: int
    ) -> dict[str, Any]:
        self.fswatcher = FileSystemWatcher(self.fscache)
        t0 = time.time()
        self.update_sources(sources)
        t1 = time.time()
        try:
            result = mypy.build.build(sources=sources, options=self.options, fscache=self.fscache)
        except mypy.errors.CompileError as e:
            output = "".join(s + "\n" for s in e.messages)
            if e.use_stdout:
                out, err = output, ""
            else:
                out, err = "", output
            return {"out": out, "err": err, "status": 2}
        messages = result.errors
        self.fine_grained_manager = FineGrainedBuildManager(result)

        original_sources_len = len(sources)
        if self.following_imports():
            sources = find_all_sources_in_build(self.fine_grained_manager.graph, sources)
            self.update_sources(sources)

        self.previous_sources = sources

        # If we are using the fine-grained cache, build hasn't actually done
        # the typechecking on the updated files yet.
        # Run a fine-grained update starting from the cached data
        if result.used_cache:
            t2 = time.time()
            # Pull times and hashes out of the saved_cache and stick them into
            # the fswatcher, so we pick up the changes.
            for state in self.fine_grained_manager.graph.values():
                meta = state.meta
                if meta is None:
                    continue
                assert state.path is not None
                self.fswatcher.set_file_data(
                    state.path,
                    FileData(st_mtime=float(meta.mtime), st_size=meta.size, hash=meta.hash),
                )

            changed, removed = self.find_changed(sources)
            changed += self.find_added_suppressed(
                self.fine_grained_manager.graph,
                set(),
                self.fine_grained_manager.manager.search_paths,
            )

            # Find anything that has had its dependency list change
            for state in self.fine_grained_manager.graph.values():
                if not state.is_fresh():
                    assert state.path is not None
                    changed.append((state.id, state.path))

            t3 = time.time()
            # Run an update
            messages = self.fine_grained_manager.update(changed, removed)

            if self.following_imports():
                # We need to do another update to any new files found by following imports.
                messages = self.fine_grained_increment_follow_imports(sources)

            t4 = time.time()
            self.fine_grained_manager.manager.add_stats(
                update_sources_time=t1 - t0,
                build_time=t2 - t1,
                find_changes_time=t3 - t2,
                fg_update_time=t4 - t3,
                files_changed=len(removed) + len(changed),
            )

        else:
            # Stores the initial state of sources as a side effect.
            self.fswatcher.find_changed()

        if MEM_PROFILE:
            from mypy.memprofile import print_memory_profile

            print_memory_profile(run_gc=False)

        __, n_notes, __ = count_stats(messages)
        status = 1 if messages and n_notes < len(messages) else 0
        # We use explicit sources length to match the logic in non-incremental mode.
        messages = self.pretty_messages(messages, original_sources_len, is_tty, terminal_width)
        return {"out": "".join(s + "\n" for s in messages), "err": "", "status": status}

    def fine_grained_increment(
        self,
        sources: list[BuildSource],
        remove: list[str] | None = None,
        update: list[str] | None = None,
        explicit_export_types: bool = False,
    ) -> list[str]:
        """Perform a fine-grained type checking increment.

        If remove and update are None, determine changed paths by using
        fswatcher. Otherwise, assume that only these files have changes.

        Args:
            sources: sources passed on the command line
            remove: paths of files that have been removed
            update: paths of files that have been changed or created
            explicit_export_types: --export-type was passed in a check command
              (as opposite to being set in dmypy start)
        """
        assert self.fine_grained_manager is not None
        manager = self.fine_grained_manager.manager

        t0 = time.time()
        if remove is None and update is None:
            # Use the fswatcher to determine which files were changed
            # (updated or added) or removed.
            self.update_sources(sources)
            changed, removed = self.find_changed(sources)
        else:
            # Use the remove/update lists to update fswatcher.
            # This avoids calling stat() for unchanged files.
            changed, removed = self.update_changed(sources, remove or [], update or [])
        if explicit_export_types:
            # If --export-types is given, we need to force full re-checking of all
            # explicitly passed files, since we need to visit each expression.
            add_all_sources_to_changed(sources, changed)
        changed += self.find_added_suppressed(
            self.fine_grained_manager.graph, set(), manager.search_paths
        )
        manager.search_paths = compute_search_paths(sources, manager.options, manager.data_dir)
        t1 = time.time()
        manager.log(f"fine-grained increment: find_changed: {t1 - t0:.3f}s")
        messages = self.fine_grained_manager.update(changed, removed)
        t2 = time.time()
        manager.log(f"fine-grained increment: update: {t2 - t1:.3f}s")
        manager.add_stats(
            find_changes_time=t1 - t0,
            fg_update_time=t2 - t1,
            files_changed=len(removed) + len(changed),
        )

        self.previous_sources = sources
        return messages

    def fine_grained_increment_follow_imports(
        self, sources: list[BuildSource], explicit_export_types: bool = False
    ) -> list[str]:
        """Like fine_grained_increment, but follow imports."""
        t0 = time.time()

        # TODO: Support file events

        assert self.fine_grained_manager is not None
        fine_grained_manager = self.fine_grained_manager
        graph = fine_grained_manager.graph
        manager = fine_grained_manager.manager

        orig_modules = list(graph.keys())

        self.update_sources(sources)
        changed_paths = self.fswatcher.find_changed()
        manager.search_paths = compute_search_paths(sources, manager.options, manager.data_dir)

        t1 = time.time()
        manager.log(f"fine-grained increment: find_changed: {t1 - t0:.3f}s")

        seen = {source.module for source in sources}

        # Find changed modules reachable from roots (or in roots) already in graph.
        changed, new_files = self.find_reachable_changed_modules(
            sources, graph, seen, changed_paths
        )
        if explicit_export_types:
            # Same as in fine_grained_increment().
            add_all_sources_to_changed(sources, changed)
        sources.extend(new_files)

        # Process changes directly reachable from roots.
        messages = fine_grained_manager.update(changed, [], followed=True)

        # Follow deps from changed modules (still within graph).
        worklist = changed.copy()
        while worklist:
            module = worklist.pop()
            if module[0] not in graph:
                continue
            sources2 = self.direct_imports(module, graph)
            # Filter anything already seen before. This prevents
            # infinite looping if there are any self edges. (Self
            # edges are maybe a bug, but...)
            sources2 = [source for source in sources2 if source.module not in seen]
            changed, new_files = self.find_reachable_changed_modules(
                sources2, graph, seen, changed_paths
            )
            self.update_sources(new_files)
            messages = fine_grained_manager.update(changed, [], followed=True)
            worklist.extend(changed)

        t2 = time.time()

        def refresh_file(module: str, path: str) -> list[str]:
            return fine_grained_manager.update([(module, path)], [], followed=True)

        for module_id, state in list(graph.items()):
            new_messages = refresh_suppressed_submodules(
                module_id, state.path, fine_grained_manager.deps, graph, self.fscache, refresh_file
            )
            if new_messages is not None:
                messages = new_messages

        t3 = time.time()

        # There may be new files that became available, currently treated as
        # suppressed imports. Process them.
        while True:
            new_unsuppressed = self.find_added_suppressed(graph, seen, manager.search_paths)
            if not new_unsuppressed:
                break
            new_files = [BuildSource(mod[1], mod[0], followed=True) for mod in new_unsuppressed]
            sources.extend(new_files)
            self.update_sources(new_files)
            messages = fine_grained_manager.update(new_unsuppressed, [], followed=True)

            for module_id, path in new_unsuppressed:
                new_messages = refresh_suppressed_submodules(
                    module_id, path, fine_grained_manager.deps, graph, self.fscache, refresh_file
                )
                if new_messages is not None:
                    messages = new_messages

        t4 = time.time()

        # Find all original modules in graph that were not reached -- they are deleted.
        to_delete = []
        for module_id in orig_modules:
            if module_id not in graph:
                continue
            if module_id not in seen:
                module_path = graph[module_id].path
                assert module_path is not None
                to_delete.append((module_id, module_path))
        if to_delete:
            messages = fine_grained_manager.update([], to_delete)

        fix_module_deps(graph)

        self.previous_sources = find_all_sources_in_build(graph)
        self.update_sources(self.previous_sources)

        # Store current file state as side effect
        self.fswatcher.find_changed()

        t5 = time.time()

        manager.log(f"fine-grained increment: update: {t5 - t1:.3f}s")
        manager.add_stats(
            find_changes_time=t1 - t0,
            fg_update_time=t2 - t1,
            refresh_suppressed_time=t3 - t2,
            find_added_supressed_time=t4 - t3,
            cleanup_time=t5 - t4,
        )

        return messages

    def find_reachable_changed_modules(
        self,
        roots: list[BuildSource],
        graph: mypy.build.Graph,
        seen: set[str],
        changed_paths: AbstractSet[str],
    ) -> tuple[list[tuple[str, str]], list[BuildSource]]:
        """Follow imports within graph from given sources until hitting changed modules.

        If we find a changed module, we can't continue following imports as the imports
        may have changed.

        Args:
            roots: modules where to start search from
            graph: module graph to use for the search
            seen: modules we've seen before that won't be visited (mutated here!!)
            changed_paths: which paths have changed (stop search here and return any found)

        Return (encountered reachable changed modules,
                unchanged files not in sources_set traversed).
        """
        changed = []
        new_files = []
        worklist = roots.copy()
        seen.update(source.module for source in worklist)
        while worklist:
            nxt = worklist.pop()
            if nxt.module not in seen:
                seen.add(nxt.module)
                new_files.append(nxt)
            if nxt.path in changed_paths:
                assert nxt.path is not None  # TODO
                changed.append((nxt.module, nxt.path))
            elif nxt.module in graph:
                state = graph[nxt.module]
                for dep in state.dependencies:
                    if dep not in seen:
                        seen.add(dep)
                        worklist.append(BuildSource(graph[dep].path, graph[dep].id, followed=True))
        return changed, new_files

    def direct_imports(
        self, module: tuple[str, str], graph: mypy.build.Graph
    ) -> list[BuildSource]:
        """Return the direct imports of module not included in seen."""
        state = graph[module[0]]
        return [BuildSource(graph[dep].path, dep, followed=True) for dep in state.dependencies]

    def find_added_suppressed(
        self, graph: mypy.build.Graph, seen: set[str], search_paths: SearchPaths
    ) -> list[tuple[str, str]]:
        """Find suppressed modules that have been added (and not included in seen).

        Args:
            seen: reachable modules we've seen before (mutated here!!)

        Return suppressed, added modules.
        """
        all_suppressed = set()
        for state in graph.values():
            all_suppressed |= state.suppressed_set

        # Filter out things that shouldn't actually be considered suppressed.
        #
        # TODO: Figure out why these are treated as suppressed
        all_suppressed = {
            module
            for module in all_suppressed
            if module not in graph and not ignore_suppressed_imports(module)
        }

        # Optimization: skip top-level packages that are obviously not
        # there, to avoid calling the relatively slow find_module()
        # below too many times.
        packages = {module.split(".", 1)[0] for module in all_suppressed}
        packages = filter_out_missing_top_level_packages(packages, search_paths, self.fscache)

        # TODO: Namespace packages

        finder = FindModuleCache(search_paths, self.fscache, self.options)

        found = []

        for module in all_suppressed:
            top_level_pkg = module.split(".", 1)[0]
            if top_level_pkg not in packages:
                # Fast path: non-existent top-level package
                continue
            result = finder.find_module(module, fast_path=True)
            if isinstance(result, str) and module not in seen:
                # When not following imports, we only follow imports to .pyi files.
                if not self.following_imports() and not result.endswith(".pyi"):
                    continue
                found.append((module, result))
                seen.add(module)

        return found

    def increment_output(
        self, messages: list[str], sources: list[BuildSource], is_tty: bool, terminal_width: int
    ) -> dict[str, Any]:
        status = 1 if messages else 0
        messages = self.pretty_messages(messages, len(sources), is_tty, terminal_width)
        return {"out": "".join(s + "\n" for s in messages), "err": "", "status": status}

    def pretty_messages(
        self,
        messages: list[str],
        n_sources: int,
        is_tty: bool = False,
        terminal_width: int | None = None,
    ) -> list[str]:
        use_color = self.options.color_output and is_tty
        fit_width = self.options.pretty and is_tty
        if fit_width:
            messages = self.formatter.fit_in_terminal(
                messages, fixed_terminal_width=terminal_width
            )
        if self.options.error_summary:
            summary: str | None = None
            n_errors, n_notes, n_files = count_stats(messages)
            if n_errors:
                summary = self.formatter.format_error(
                    n_errors, n_files, n_sources, use_color=use_color
                )
            elif not messages or n_notes == len(messages):
                summary = self.formatter.format_success(n_sources, use_color)
            if summary:
                # Create new list to avoid appending multiple summaries on successive runs.
                messages = messages + [summary]
        if use_color:
            messages = [self.formatter.colorize(m) for m in messages]
        return messages

    def update_sources(self, sources: list[BuildSource]) -> None:
        paths = [source.path for source in sources if source.path is not None]
        if self.following_imports():
            # Filter out directories (used for namespace packages).
            paths = [path for path in paths if self.fscache.isfile(path)]
        self.fswatcher.add_watched_paths(paths)

    def update_changed(
        self, sources: list[BuildSource], remove: list[str], update: list[str]
    ) -> ChangesAndRemovals:
        changed_paths = self.fswatcher.update_changed(remove, update)
        return self._find_changed(sources, changed_paths)

    def find_changed(self, sources: list[BuildSource]) -> ChangesAndRemovals:
        changed_paths = self.fswatcher.find_changed()
        return self._find_changed(sources, changed_paths)

    def _find_changed(
        self, sources: list[BuildSource], changed_paths: AbstractSet[str]
    ) -> ChangesAndRemovals:
        # Find anything that has been added or modified
        changed = [
            (source.module, source.path)
            for source in sources
            if source.path and source.path in changed_paths
        ]

        # Now find anything that has been removed from the build
        modules = {source.module for source in sources}
        omitted = [source for source in self.previous_sources if source.module not in modules]
        removed = []
        for source in omitted:
            path = source.path
            assert path
            removed.append((source.module, path))

        # Always add modules that were (re-)added, since they may be detected as not changed by
        # fswatcher (if they were actually not changed), but they may still need to be checked
        # in case they had errors before they were deleted from sources on previous runs.
        previous_modules = {source.module for source in self.previous_sources}
        changed_set = set(changed)
        changed.extend(
            [
                (source.module, source.path)
                for source in sources
                if source.path
                and source.module not in previous_modules
                and (source.module, source.path) not in changed_set
            ]
        )

        # Find anything that has had its module path change because of added or removed __init__s
        last = {s.path: s.module for s in self.previous_sources}
        for s in sources:
            assert s.path
            if s.path in last and last[s.path] != s.module:
                # Mark it as removed from its old name and changed at its new name
                removed.append((last[s.path], s.path))
                changed.append((s.module, s.path))

        return changed, removed

    def cmd_inspect(
        self,
        show: str,
        location: str,
        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,
    ) -> dict[str, object]:
        """Locate and inspect expression(s)."""
        if not self.fine_grained_manager:
            return {
                "error": 'Command "inspect" is only valid after a "check" command'
                " (that produces no parse errors)"
            }
        engine = InspectionEngine(
            self.fine_grained_manager,
            verbosity=verbosity,
            limit=limit,
            include_span=include_span,
            include_kind=include_kind,
            include_object_attrs=include_object_attrs,
            union_attrs=union_attrs,
            force_reload=force_reload,
        )
        old_inspections = self.options.inspections
        self.options.inspections = True
        try:
            if show == "type":
                result = engine.get_type(location)
            elif show == "attrs":
                result = engine.get_attrs(location)
            elif show == "definition":
                result = engine.get_definition(location)
            else:
                assert False, "Unknown inspection kind"
        finally:
            self.options.inspections = old_inspections
        if "out" in result:
            assert isinstance(result["out"], str)
            result["out"] += "\n"
        return result

    def cmd_suggest(self, function: str, callsites: bool, **kwargs: Any) -> dict[str, object]:
        """Suggest a signature for a function."""
        if not self.fine_grained_manager:
            return {
                "error": "Command 'suggest' is only valid after a 'check' command"
                " (that produces no parse errors)"
            }
        engine = SuggestionEngine(self.fine_grained_manager, **kwargs)
        try:
            if callsites:
                out = engine.suggest_callsites(function)
            else:
                out = engine.suggest(function)
        except SuggestionFailure as err:
            return {"error": str(err)}
        else:
            if not out:
                out = "No suggestions\n"
            elif not out.endswith("\n"):
                out += "\n"
            return {"out": out, "err": "", "status": 0}
        finally:
            self.flush_caches()

    def cmd_hang(self) -> dict[str, object]:
        """Hang for 100 seconds, as a debug hack."""
        time.sleep(100)
        return {}

# Misc utilities.

MiB: Final = 2**20

def get_meminfo() -> dict[str, Any]:
    res: dict[str, Any] = {}
    try:
        import psutil
    except ImportError:
        res["memory_psutil_missing"] = (
            "psutil not found, run pip install mypy[dmypy] "
            "to install the needed components for dmypy"
        )
    else:
        process = psutil.Process()
        meminfo = process.memory_info()
        res["memory_rss_mib"] = meminfo.rss / MiB
        res["memory_vms_mib"] = meminfo.vms / MiB
        if sys.platform == "win32":
            res["memory_maxrss_mib"] = meminfo.peak_wset / MiB
        else:
            # See https://stackoverflow.com/questions/938733/total-memory-used-by-python-process
            import resource  # Since it doesn't exist on Windows.

            rusage = resource.getrusage(resource.RUSAGE_SELF)
            if sys.platform == "darwin":
                factor = 1
            else:
                factor = 1024  # Linux
            res["memory_maxrss_mib"] = rusage.ru_maxrss * factor / MiB
    return res

def find_all_sources_in_build(
    graph: mypy.build.Graph, extra: Sequence[BuildSource] = ()
) -> list[BuildSource]:
    result = list(extra)
    seen = {source.module for source in result}
    for module, state in graph.items():
        if module not in seen:
            result.append(BuildSource(state.path, module))
    return result

def add_all_sources_to_changed(sources: list[BuildSource], changed: list[tuple[str, str]]) -> None:
    """Add all (explicit) sources to the list changed files in place.

    Use this when re-processing of unchanged files is needed (e.g. for
    the purpose of exporting types for inspections).
    """
    changed_set = set(changed)
    changed.extend(
        [
            (bs.module, bs.path)
            for bs in sources
            if bs.path and (bs.module, bs.path) not in changed_set
        ]
    )

def fix_module_deps(graph: mypy.build.Graph) -> None:
    """After an incremental update, update module dependencies to reflect the new state.

    This can make some suppressed dependencies non-suppressed, and vice versa (if modules
    have been added to or removed from the build).
    """
    for state in graph.values():
        new_suppressed = []
        new_dependencies = []
        for dep in state.dependencies + state.suppressed:
            if dep in graph:
                new_dependencies.append(dep)
            else:
                new_suppressed.append(dep)
        state.dependencies = new_dependencies
        state.dependencies_set = set(new_dependencies)
        state.suppressed = new_suppressed
        state.suppressed_set = set(new_suppressed)

def filter_out_missing_top_level_packages(
    packages: set[str], search_paths: SearchPaths, fscache: FileSystemCache
) -> set[str]:
    """Quickly filter out obviously missing top-level packages.

    Return packages with entries that can't be found removed.

    This is approximate: some packages that aren't actually valid may be
    included. However, all potentially valid packages must be returned.
    """
    # Start with a empty set and add all potential top-level packages.
    found = set()
    paths = (
        search_paths.python_path
        + search_paths.mypy_path
        + search_paths.package_path
        + search_paths.typeshed_path
    )
    for p in paths:
        try:
            entries = fscache.listdir(p)
        except Exception:
            entries = []
        for entry in entries:
            # The code is hand-optimized for mypyc since this may be somewhat
            # performance-critical.
            if entry.endswith(".py"):
                entry = entry[:-3]
            elif entry.endswith(".pyi"):
                entry = entry[:-4]
            elif entry.endswith("-stubs"):
                # Possible PEP 561 stub package
                entry = entry[:-6]
            if entry in packages:
                found.add(entry)
    return found


"""Shared code between dmypy.py and dmypy_server.py.

This should be pretty lightweight and not depend on other mypy code (other than ipc).
"""

from __future__ import annotations

import io
import json
from types import TracebackType
from typing import Any, Final, Iterable, Iterator, TextIO

from mypy.ipc import IPCBase

DEFAULT_STATUS_FILE: Final = ".dmypy.json"

def receive(connection: IPCBase) -> Any:
    """Receive single JSON data frame from a connection.

    Raise OSError if the data received is not valid JSON or if it is
    not a dict.
    """
    bdata = connection.read()
    if not bdata:
        raise OSError("No data received")
    try:
        data = json.loads(bdata)
    except Exception as e:
        raise OSError("Data received is not valid JSON") from e
    if not isinstance(data, dict):
        raise OSError(f"Data received is not a dict ({type(data)})")
    return data

def send(connection: IPCBase, data: Any) -> None:
    """Send data to a connection encoded and framed.

    The data must be JSON-serializable. We assume that a single send call is a
    single frame to be sent on the connect.
    """
    connection.write(json.dumps(data))

class WriteToConn(TextIO):
    """Helper class to write to a connection instead of standard output."""

    def __init__(self, server: IPCBase, output_key: str, isatty: bool) -> None:
        self.server = server
        self.output_key = output_key
        self._isatty = isatty

    def __enter__(self) -> TextIO:
        return self

    def __exit__(
        self,
        t: type[BaseException] | None,
        value: BaseException | None,
        traceback: TracebackType | None,
    ) -> None:
        pass

    def __iter__(self) -> Iterator[str]:
        raise io.UnsupportedOperation

    def __next__(self) -> str:
        raise io.UnsupportedOperation

    def close(self) -> None:
        pass

    def fileno(self) -> int:
        raise OSError

    def flush(self) -> None:
        pass

    def isatty(self) -> bool:
        return self._isatty

    def read(self, n: int = 0) -> str:
        raise io.UnsupportedOperation

    def readable(self) -> bool:
        return False

    def readline(self, limit: int = 0) -> str:
        raise io.UnsupportedOperation

    def readlines(self, hint: int = 0) -> list[str]:
        raise io.UnsupportedOperation

    def seek(self, offset: int, whence: int = 0) -> int:
        raise io.UnsupportedOperation

    def seekable(self) -> bool:
        return False

    def tell(self) -> int:
        raise io.UnsupportedOperation

    def truncate(self, size: int | None = 0) -> int:
        raise io.UnsupportedOperation

    def write(self, output: str) -> int:
        resp: dict[str, Any] = {}
        resp[self.output_key] = output
        send(self.server, resp)
        return len(output)

    def writable(self) -> bool:
        return True

    def writelines(self, lines: Iterable[str]) -> None:
        for s in lines:
            self.write(s)


from __future__ import annotations

from typing import Callable, Container, cast

from mypy.nodes import ARG_STAR, ARG_STAR2
from mypy.types import (
    AnyType,
    CallableType,
    DeletedType,
    ErasedType,
    Instance,
    LiteralType,
    NoneType,
    Overloaded,
    Parameters,
    ParamSpecType,
    PartialType,
    ProperType,
    TupleType,
    Type,
    TypeAliasType,
    TypedDictType,
    TypeOfAny,
    TypeTranslator,
    TypeType,
    TypeVarId,
    TypeVarTupleType,
    TypeVarType,
    TypeVisitor,
    UnboundType,
    UninhabitedType,
    UnionType,
    UnpackType,
    get_proper_type,
    get_proper_types,
)
from mypy.typevartuples import erased_vars

def erase_type(typ: Type) -> ProperType:
    """Erase any type variables from a type.

    Also replace tuple types with the corresponding concrete types.

    Examples:
      A -> A
      B[X] -> B[Any]
      Tuple[A, B] -> tuple
      Callable[[A1, A2, ...], R] -> Callable[..., Any]
      Type[X] -> Type[Any]
    """
    typ = get_proper_type(typ)
    return typ.accept(EraseTypeVisitor())

class EraseTypeVisitor(TypeVisitor[ProperType]):
    def visit_unbound_type(self, t: UnboundType) -> ProperType:
        # TODO: replace with an assert after UnboundType can't leak from semantic analysis.
        return AnyType(TypeOfAny.from_error)

    def visit_any(self, t: AnyType) -> ProperType:
        return t

    def visit_none_type(self, t: NoneType) -> ProperType:
        return t

    def visit_uninhabited_type(self, t: UninhabitedType) -> ProperType:
        return t

    def visit_erased_type(self, t: ErasedType) -> ProperType:
        return t

    def visit_partial_type(self, t: PartialType) -> ProperType:
        # Should not get here.
        raise RuntimeError("Cannot erase partial types")

    def visit_deleted_type(self, t: DeletedType) -> ProperType:
        return t

    def visit_instance(self, t: Instance) -> ProperType:
        args = erased_vars(t.type.defn.type_vars, TypeOfAny.special_form)
        return Instance(t.type, args, t.line)

    def visit_type_var(self, t: TypeVarType) -> ProperType:
        return AnyType(TypeOfAny.special_form)

    def visit_param_spec(self, t: ParamSpecType) -> ProperType:
        return AnyType(TypeOfAny.special_form)

    def visit_parameters(self, t: Parameters) -> ProperType:
        raise RuntimeError("Parameters should have been bound to a class")

    def visit_type_var_tuple(self, t: TypeVarTupleType) -> ProperType:
        # Likely, we can never get here because of aggressive erasure of types that
        # can contain this, but better still return a valid replacement.
        return t.tuple_fallback.copy_modified(args=[AnyType(TypeOfAny.special_form)])

    def visit_unpack_type(self, t: UnpackType) -> ProperType:
        return AnyType(TypeOfAny.special_form)

    def visit_callable_type(self, t: CallableType) -> ProperType:
        # We must preserve the fallback type for overload resolution to work.
        any_type = AnyType(TypeOfAny.special_form)
        return CallableType(
            arg_types=[any_type, any_type],
            arg_kinds=[ARG_STAR, ARG_STAR2],
            arg_names=[None, None],
            ret_type=any_type,
            fallback=t.fallback,
            is_ellipsis_args=True,
            implicit=True,
        )

    def visit_overloaded(self, t: Overloaded) -> ProperType:
        return t.fallback.accept(self)

    def visit_tuple_type(self, t: TupleType) -> ProperType:
        return t.partial_fallback.accept(self)

    def visit_typeddict_type(self, t: TypedDictType) -> ProperType:
        return t.fallback.accept(self)

    def visit_literal_type(self, t: LiteralType) -> ProperType:
        # The fallback for literal types should always be either
        # something like int or str, or an enum class -- types that
        # don't contain any TypeVars. So there's no need to visit it.
        return t

    def visit_union_type(self, t: UnionType) -> ProperType:
        erased_items = [erase_type(item) for item in t.items]
        from mypy.typeops import make_simplified_union

        return make_simplified_union(erased_items)

    def visit_type_type(self, t: TypeType) -> ProperType:
        return TypeType.make_normalized(t.item.accept(self), line=t.line)

    def visit_type_alias_type(self, t: TypeAliasType) -> ProperType:
        raise RuntimeError("Type aliases should be expanded before accepting this visitor")

def erase_typevars(t: Type, ids_to_erase: Container[TypeVarId] | None = None) -> Type:
    """Replace all type variables in a type with any,
    or just the ones in the provided collection.
    """

    def erase_id(id: TypeVarId) -> bool:
        if ids_to_erase is None:
            return True
        return id in ids_to_erase

    return t.accept(TypeVarEraser(erase_id, AnyType(TypeOfAny.special_form)))

def replace_meta_vars(t: Type, target_type: Type) -> Type:
    """Replace unification variables in a type with the target type."""
    return t.accept(TypeVarEraser(lambda id: id.is_meta_var(), target_type))

class TypeVarEraser(TypeTranslator):
    """Implementation of type erasure"""

    def __init__(self, erase_id: Callable[[TypeVarId], bool], replacement: Type) -> None:
        super().__init__()
        self.erase_id = erase_id
        self.replacement = replacement

    def visit_type_var(self, t: TypeVarType) -> Type:
        if self.erase_id(t.id):
            return self.replacement
        return t

    # TODO: below two methods duplicate some logic with expand_type().
    # In fact, we may want to refactor this whole visitor to use expand_type().
    def visit_instance(self, t: Instance) -> Type:
        result = super().visit_instance(t)
        assert isinstance(result, ProperType) and isinstance(result, Instance)
        if t.type.fullname == "builtins.tuple":
            # Normalize Tuple[*Tuple[X, ...], ...] -> Tuple[X, ...]
            arg = result.args[0]
            if isinstance(arg, UnpackType):
                unpacked = get_proper_type(arg.type)
                if isinstance(unpacked, Instance):
                    assert unpacked.type.fullname == "builtins.tuple"
                    return unpacked
        return result

    def visit_tuple_type(self, t: TupleType) -> Type:
        result = super().visit_tuple_type(t)
        assert isinstance(result, ProperType) and isinstance(result, TupleType)
        if len(result.items) == 1:
            # Normalize Tuple[*Tuple[X, ...]] -> Tuple[X, ...]
            item = result.items[0]
            if isinstance(item, UnpackType):
                unpacked = get_proper_type(item.type)
                if isinstance(unpacked, Instance):
                    assert unpacked.type.fullname == "builtins.tuple"
                    if result.partial_fallback.type.fullname != "builtins.tuple":
                        # If it is a subtype (like named tuple) we need to preserve it,
                        # this essentially mimics the logic in tuple_fallback().
                        return result.partial_fallback.accept(self)
                    return unpacked
        return result

    def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type:
        if self.erase_id(t.id):
            return t.tuple_fallback.copy_modified(args=[self.replacement])
        return t

    def visit_param_spec(self, t: ParamSpecType) -> Type:
        if self.erase_id(t.id):
            return self.replacement
        return t

    def visit_type_alias_type(self, t: TypeAliasType) -> Type:
        # Type alias target can't contain bound type variables (not bound by the type
        # alias itself), so it is safe to just erase the arguments.
        return t.copy_modified(args=[a.accept(self) for a in t.args])

def remove_instance_last_known_values(t: Type) -> Type:
    return t.accept(LastKnownValueEraser())

class LastKnownValueEraser(TypeTranslator):
    """Removes the Literal[...] type that may be associated with any
    Instance types."""

    def visit_instance(self, t: Instance) -> Type:
        if not t.last_known_value and not t.args:
            return t
        return t.copy_modified(args=[a.accept(self) for a in t.args], last_known_value=None)

    def visit_type_alias_type(self, t: TypeAliasType) -> Type:
        # Type aliases can't contain literal values, because they are
        # always constructed as explicit types.
        return t

    def visit_union_type(self, t: UnionType) -> Type:
        new = cast(UnionType, super().visit_union_type(t))
        # Erasure can result in many duplicate items; merge them.
        # Call make_simplified_union only on lists of instance types
        # that all have the same fullname, to avoid simplifying too
        # much.
        instances = [item for item in new.items if isinstance(get_proper_type(item), Instance)]
        # Avoid merge in simple cases such as optional types.
        if len(instances) > 1:
            instances_by_name: dict[str, list[Instance]] = {}
            p_new_items = get_proper_types(new.items)
            for p_item in p_new_items:
                if isinstance(p_item, Instance) and not p_item.args:
                    instances_by_name.setdefault(p_item.type.fullname, []).append(p_item)
            merged: list[Type] = []
            for item in new.items:
                orig_item = item
                item = get_proper_type(item)
                if isinstance(item, Instance) and not item.args:
                    types = instances_by_name.get(item.type.fullname)
                    if types is not None:
                        if len(types) == 1:
                            merged.append(item)
                        else:
                            from mypy.typeops import make_simplified_union

                            merged.append(make_simplified_union(types))
                            del instances_by_name[item.type.fullname]
                else:
                    merged.append(orig_item)
            return UnionType.make_union(merged)
        return new


"""Defines the different custom formats in which mypy can output."""

import json
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from mypy.errors import MypyError

class ErrorFormatter(ABC):
    """Base class to define how errors are formatted before being printed."""

    @abstractmethod
    def report_error(self, error: "MypyError") -> str:
        raise NotImplementedError

class JSONFormatter(ErrorFormatter):
    """Formatter for basic JSON output format."""

    def report_error(self, error: "MypyError") -> str:
        """Prints out the errors as simple, static JSON lines."""
        return json.dumps(
            {
                "file": error.file_path,
                "line": error.line,
                "column": error.column,
                "message": error.message,
                "hint": None if len(error.hints) == 0 else "\n".join(error.hints),
                "code": None if error.errorcode is None else error.errorcode.code,
                "severity": error.severity,
            }
        )

OUTPUT_CHOICES = {"json": JSONFormatter()}


"""Classification of possible errors mypy can detect.

These can be used for filtering specific errors.
"""

from __future__ import annotations

from collections import defaultdict
from typing import Final

from mypy_extensions import mypyc_attr

error_codes: dict[str, ErrorCode] = {}
sub_code_map: dict[str, set[str]] = defaultdict(set)

@mypyc_attr(allow_interpreted_subclasses=True)
class ErrorCode:
    def __init__(
        self,
        code: str,
        description: str,
        category: str,
        default_enabled: bool = True,
        sub_code_of: ErrorCode | None = None,
    ) -> None:
        self.code = code
        self.description = description
        self.category = category
        self.default_enabled = default_enabled
        self.sub_code_of = sub_code_of
        if sub_code_of is not None:
            assert sub_code_of.sub_code_of is None, "Nested subcategories are not supported"
            sub_code_map[sub_code_of.code].add(code)
        error_codes[code] = self

    def __str__(self) -> str:
        return f"<ErrorCode {self.code}>"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, ErrorCode):
            return False
        return self.code == other.code

    def __hash__(self) -> int:
        return hash((self.code,))

ATTR_DEFINED: Final = ErrorCode("attr-defined", "Check that attribute exists", "General")
NAME_DEFINED: Final = ErrorCode("name-defined", "Check that name is defined", "General")
CALL_ARG: Final[ErrorCode] = ErrorCode(
    "call-arg", "Check number, names and kinds of arguments in calls", "General"
)
ARG_TYPE: Final = ErrorCode("arg-type", "Check argument types in calls", "General")
CALL_OVERLOAD: Final = ErrorCode(
    "call-overload", "Check that an overload variant matches arguments", "General"
)
VALID_TYPE: Final[ErrorCode] = ErrorCode(
    "valid-type", "Check that type (annotation) is valid", "General"
)
VAR_ANNOTATED: Final = ErrorCode(
    "var-annotated", "Require variable annotation if type can't be inferred", "General"
)
OVERRIDE: Final = ErrorCode(
    "override", "Check that method override is compatible with base class", "General"
)
RETURN: Final[ErrorCode] = ErrorCode(
    "return", "Check that function always returns a value", "General"
)
RETURN_VALUE: Final[ErrorCode] = ErrorCode(
    "return-value", "Check that return value is compatible with signature", "General"
)
ASSIGNMENT: Final[ErrorCode] = ErrorCode(
    "assignment", "Check that assigned value is compatible with target", "General"
)
METHOD_ASSIGN: Final[ErrorCode] = ErrorCode(
    "method-assign",
    "Check that assignment target is not a method",
    "General",
    sub_code_of=ASSIGNMENT,
)
TYPE_ARG: Final = ErrorCode("type-arg", "Check that generic type arguments are present", "General")
TYPE_VAR: Final = ErrorCode("type-var", "Check that type variable values are valid", "General")
UNION_ATTR: Final = ErrorCode(
    "union-attr", "Check that attribute exists in each item of a union", "General"
)
INDEX: Final = ErrorCode("index", "Check indexing operations", "General")
OPERATOR: Final = ErrorCode("operator", "Check that operator is valid for operands", "General")
LIST_ITEM: Final = ErrorCode(
    "list-item", "Check list items in a list expression [item, ...]", "General"
)
DICT_ITEM: Final = ErrorCode(
    "dict-item", "Check dict items in a dict expression {key: value, ...}", "General"
)
TYPEDDICT_ITEM: Final = ErrorCode(
    "typeddict-item", "Check items when constructing TypedDict", "General"
)
TYPEDDICT_UNKNOWN_KEY: Final = ErrorCode(
    "typeddict-unknown-key",
    "Check unknown keys when constructing TypedDict",
    "General",
    sub_code_of=TYPEDDICT_ITEM,
)
HAS_TYPE: Final = ErrorCode(
    "has-type", "Check that type of reference can be determined", "General"
)
IMPORT: Final = ErrorCode(
    "import", "Require that imported module can be found or has stubs", "General"
)
IMPORT_NOT_FOUND: Final = ErrorCode(
    "import-not-found", "Require that imported module can be found", "General", sub_code_of=IMPORT
)
IMPORT_UNTYPED: Final = ErrorCode(
    "import-untyped", "Require that imported module has stubs", "General", sub_code_of=IMPORT
)
NO_REDEF: Final = ErrorCode("no-redef", "Check that each name is defined once", "General")
FUNC_RETURNS_VALUE: Final = ErrorCode(
    "func-returns-value", "Check that called function returns a value in value context", "General"
)
ABSTRACT: Final = ErrorCode(
    "abstract", "Prevent instantiation of classes with abstract attributes", "General"
)
TYPE_ABSTRACT: Final = ErrorCode(
    "type-abstract", "Require only concrete classes where Type[...] is expected", "General"
)
VALID_NEWTYPE: Final = ErrorCode(
    "valid-newtype", "Check that argument 2 to NewType is valid", "General"
)
STRING_FORMATTING: Final = ErrorCode(
    "str-format", "Check that string formatting/interpolation is type-safe", "General"
)
STR_BYTES_PY3: Final = ErrorCode(
    "str-bytes-safe", "Warn about implicit coercions related to bytes and string types", "General"
)
EXIT_RETURN: Final = ErrorCode(
    "exit-return", "Warn about too general return type for '__exit__'", "General"
)
LITERAL_REQ: Final = ErrorCode("literal-required", "Check that value is a literal", "General")
UNUSED_COROUTINE: Final = ErrorCode(
    "unused-coroutine", "Ensure that all coroutines are used", "General"
)
# TODO: why do we need the explicit type here? Without it mypyc CI builds fail with
# mypy/message_registry.py:37: error: Cannot determine type of "EMPTY_BODY"  [has-type]
EMPTY_BODY: Final[ErrorCode] = ErrorCode(
    "empty-body",
    "A dedicated error code to opt out return errors for empty/trivial bodies",
    "General",
)
SAFE_SUPER: Final = ErrorCode(
    "safe-super", "Warn about calls to abstract methods with empty/trivial bodies", "General"
)
TOP_LEVEL_AWAIT: Final = ErrorCode(
    "top-level-await", "Warn about top level await expressions", "General"
)
AWAIT_NOT_ASYNC: Final = ErrorCode(
    "await-not-async", 'Warn about "await" outside coroutine ("async def")', "General"
)
# These error codes aren't enabled by default.
NO_UNTYPED_DEF: Final[ErrorCode] = ErrorCode(
    "no-untyped-def", "Check that every function has an annotation", "General"
)
NO_UNTYPED_CALL: Final = ErrorCode(
    "no-untyped-call",
    "Disallow calling functions without type annotations from annotated functions",
    "General",
)
REDUNDANT_CAST: Final = ErrorCode(
    "redundant-cast", "Check that cast changes type of expression", "General"
)
ASSERT_TYPE: Final = ErrorCode("assert-type", "Check that assert_type() call succeeds", "General")
COMPARISON_OVERLAP: Final = ErrorCode(
    "comparison-overlap", "Check that types in comparisons and 'in' expressions overlap", "General"
)
NO_ANY_UNIMPORTED: Final = ErrorCode(
    "no-any-unimported", 'Reject "Any" types from unfollowed imports', "General"
)
NO_ANY_RETURN: Final = ErrorCode(
    "no-any-return",
    'Reject returning value with "Any" type if return type is not "Any"',
    "General",
)
UNREACHABLE: Final = ErrorCode(
    "unreachable", "Warn about unreachable statements or expressions", "General"
)
ANNOTATION_UNCHECKED = ErrorCode(
    "annotation-unchecked", "Notify about type annotations in unchecked functions", "General"
)
TYPEDDICT_READONLY_MUTATED = ErrorCode(
    "typeddict-readonly-mutated", "TypedDict's ReadOnly key is mutated", "General"
)
POSSIBLY_UNDEFINED: Final[ErrorCode] = ErrorCode(
    "possibly-undefined",
    "Warn about variables that are defined only in some execution paths",
    "General",
    default_enabled=False,
)
REDUNDANT_EXPR: Final = ErrorCode(
    "redundant-expr", "Warn about redundant expressions", "General", default_enabled=False
)
TRUTHY_BOOL: Final[ErrorCode] = ErrorCode(
    "truthy-bool",
    "Warn about expressions that could always evaluate to true in boolean contexts",
    "General",
    default_enabled=False,
)
TRUTHY_FUNCTION: Final[ErrorCode] = ErrorCode(
    "truthy-function",
    "Warn about function that always evaluate to true in boolean contexts",
    "General",
)
TRUTHY_ITERABLE: Final[ErrorCode] = ErrorCode(
    "truthy-iterable",
    "Warn about Iterable expressions that could always evaluate to true in boolean contexts",
    "General",
    default_enabled=False,
)
NAME_MATCH: Final = ErrorCode(
    "name-match", "Check that type definition has consistent naming", "General"
)
NO_OVERLOAD_IMPL: Final = ErrorCode(
    "no-overload-impl",
    "Check that overloaded functions outside stub files have an implementation",
    "General",
)
IGNORE_WITHOUT_CODE: Final = ErrorCode(
    "ignore-without-code",
    "Warn about '# type: ignore' comments which do not have error codes",
    "General",
    default_enabled=False,
)
UNUSED_AWAITABLE: Final = ErrorCode(
    "unused-awaitable",
    "Ensure that all awaitable values are used",
    "General",
    default_enabled=False,
)
REDUNDANT_SELF_TYPE = ErrorCode(
    "redundant-self",
    "Warn about redundant Self type annotations on method first argument",
    "General",
    default_enabled=False,
)
USED_BEFORE_DEF: Final[ErrorCode] = ErrorCode(
    "used-before-def", "Warn about variables that are used before they are defined", "General"
)
UNUSED_IGNORE: Final = ErrorCode(
    "unused-ignore", "Ensure that all type ignores are used", "General", default_enabled=False
)
EXPLICIT_OVERRIDE_REQUIRED: Final = ErrorCode(
    "explicit-override",
    "Require @override decorator if method is overriding a base class method",
    "General",
    default_enabled=False,
)
UNIMPORTED_REVEAL: Final = ErrorCode(
    "unimported-reveal",
    "Require explicit import from typing or typing_extensions for reveal_type",
    "General",
    default_enabled=False,
)
MUTABLE_OVERRIDE: Final[ErrorCode] = ErrorCode(
    "mutable-override",
    "Reject covariant overrides for mutable attributes",
    "General",
    default_enabled=False,
)

# Syntax errors are often blocking.
SYNTAX: Final[ErrorCode] = ErrorCode("syntax", "Report syntax errors", "General")

# This is an internal marker code for a whole-file ignore. It is not intended to
# be user-visible.
FILE: Final = ErrorCode("file", "Internal marker for a whole file being ignored", "General")
del error_codes[FILE.code]

# This is a catch-all for remaining uncategorized errors.
MISC: Final[ErrorCode] = ErrorCode("misc", "Miscellaneous other checks", "General")

OVERLOAD_CANNOT_MATCH: Final[ErrorCode] = ErrorCode(
    "overload-cannot-match",
    "Warn if an @overload signature can never be matched",
    "General",
    sub_code_of=MISC,
)

OVERLOAD_OVERLAP: Final[ErrorCode] = ErrorCode(
    "overload-overlap",
    "Warn if multiple @overload variants overlap in unsafe ways",
    "General",
    sub_code_of=MISC,
)

PROPERTY_DECORATOR = ErrorCode(
    "prop-decorator",
    "Decorators on top of @property are not supported",
    "General",
    sub_code_of=MISC,
)

NARROWED_TYPE_NOT_SUBTYPE: Final[ErrorCode] = ErrorCode(
    "narrowed-type-not-subtype",
    "Warn if a TypeIs function's narrowed type is not a subtype of the original type",
    "General",
)

# This copy will not include any error codes defined later in the plugins.
mypy_error_codes = error_codes.copy()