client/commands/incremental.py (174 lines of code) (raw):
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
import json
import logging
from pathlib import Path
from typing import Iterable, Dict, Sequence, List, Optional
from .. import (
command_arguments,
configuration as configuration_module,
error,
statistics_logger,
)
from . import (
commands,
server_connection,
server_event,
start,
remote_logging as remote_logging_module,
backend_arguments,
)
LOG: logging.Logger = logging.getLogger(__name__)
COMMAND_NAME = "incremental"
class InvalidServerResponse(Exception):
pass
def parse_type_error_response_json(response_json: object) -> List[error.Error]:
try:
# The response JSON is expected to have the following form:
# `["TypeErrors", [error_json0, error_json1, ...]]`
if (
isinstance(response_json, list)
and len(response_json) > 1
and response_json[0] == "TypeErrors"
):
errors_json = response_json[1]
if isinstance(errors_json, list):
return [error.Error.from_json(error_json) for error_json in errors_json]
raise InvalidServerResponse(
f"Unexpected JSON response from server: {response_json}"
)
except error.ErrorParsingFailure as parsing_error:
message = f"Unexpected error JSON from server: {parsing_error}"
raise InvalidServerResponse(message) from parsing_error
def parse_type_error_response(response: str) -> List[error.Error]:
try:
response_json = json.loads(response)
return parse_type_error_response_json(response_json)
except json.JSONDecodeError as decode_error:
message = f"Cannot parse response as JSON: {decode_error}"
raise InvalidServerResponse(message) from decode_error
def _read_type_errors(socket_path: Path) -> List[error.Error]:
with server_connection.connect_in_text_mode(socket_path) as (
input_channel,
output_channel,
):
# The empty list argument means we want all type errors from the server.
output_channel.write('["DisplayTypeError", []]\n')
return parse_type_error_response(input_channel.readline())
def compute_error_statistics_per_code(
type_errors: Sequence[error.Error],
) -> Iterable[Dict[str, int]]:
errors_grouped_by_code: Dict[int, List[error.Error]] = {}
for type_error in type_errors:
errors_grouped_by_code.setdefault(type_error.code, []).append(type_error)
for code, errors in errors_grouped_by_code.items():
yield {"code": code, "count": len(errors)}
def log_error_statistics(
remote_logging: Optional[backend_arguments.RemoteLogging],
type_errors: Sequence[error.Error],
command_name: str,
) -> None:
if remote_logging is None:
return
logger = remote_logging.logger
if logger is None:
return
log_identifier = remote_logging.identifier
for integers in compute_error_statistics_per_code(type_errors):
statistics_logger.log(
category=statistics_logger.LoggerCategory.ERROR_STATISTICS,
logger=logger,
integers=integers,
normals={
"command": command_name,
**(
{"identifier": log_identifier} if log_identifier is not None else {}
),
},
)
def display_type_errors(errors: List[error.Error], output: str) -> None:
error.print_errors(
[error.relativize_path(against=Path.cwd()) for error in errors],
output=output,
)
def _show_progress_log_and_display_type_errors(
log_path: Path,
socket_path: Path,
output: str,
remote_logging: Optional[backend_arguments.RemoteLogging],
) -> commands.ExitCode:
LOG.info("Waiting for server...")
with start.background_logging(log_path):
type_errors = _read_type_errors(socket_path)
log_error_statistics(
remote_logging=remote_logging,
type_errors=type_errors,
command_name=COMMAND_NAME,
)
display_type_errors(type_errors, output=output)
return (
commands.ExitCode.SUCCESS
if len(type_errors) == 0
else commands.ExitCode.FOUND_ERRORS
)
def run_incremental(
configuration: configuration_module.Configuration,
incremental_arguments: command_arguments.IncrementalArguments,
) -> remote_logging_module.ExitCodeWithAdditionalLogging:
socket_path = server_connection.get_default_socket_path(
project_root=Path(configuration.project_root),
relative_local_root=Path(configuration.relative_local_root)
if configuration.relative_local_root
else None,
)
# Need to be consistent with the log symlink location in start command
log_path = Path(configuration.log_directory) / "new_server" / "server.stderr"
output = incremental_arguments.output
remote_logging = backend_arguments.RemoteLogging.create(
configuration.logger,
incremental_arguments.start_arguments.log_identifier,
)
try:
exit_code = _show_progress_log_and_display_type_errors(
log_path, socket_path, output, remote_logging
)
return remote_logging_module.ExitCodeWithAdditionalLogging(
exit_code=exit_code,
additional_logging={
"connected_to": "already_running_server",
},
)
except server_connection.ConnectionFailure:
pass
if incremental_arguments.no_start:
raise commands.ClientException("Cannot find a running Pyre server.")
LOG.info("Cannot find a running Pyre server. Starting a new one...")
start_status = start.run_start(configuration, incremental_arguments.start_arguments)
if start_status != commands.ExitCode.SUCCESS:
raise commands.ClientException(
f"`pyre start` failed with non-zero exit code: {start_status}"
)
exit_code = _show_progress_log_and_display_type_errors(
log_path, socket_path, output, remote_logging
)
return remote_logging_module.ExitCodeWithAdditionalLogging(
exit_code=exit_code,
additional_logging={
"connected_to": "newly_started_server",
},
)
def _exit_code_from_error_kind(error_kind: server_event.ErrorKind) -> commands.ExitCode:
if error_kind == server_event.ErrorKind.WATCHMAN:
return commands.ExitCode.WATCHMAN_ERROR
elif error_kind == server_event.ErrorKind.BUCK_INTERNAL:
return commands.ExitCode.BUCK_INTERNAL_ERROR
elif error_kind == server_event.ErrorKind.BUCK_USER:
return commands.ExitCode.BUCK_USER_ERROR
return commands.ExitCode.FAILURE
@remote_logging_module.log_usage_with_additional_info(command_name=COMMAND_NAME)
def run(
configuration: configuration_module.Configuration,
incremental_arguments: command_arguments.IncrementalArguments,
) -> remote_logging_module.ExitCodeWithAdditionalLogging:
try:
return run_incremental(configuration, incremental_arguments)
except server_event.ServerStartException as error:
raise commands.ClientException(
f"{error}", exit_code=_exit_code_from_error_kind(error.kind)
)
except Exception as error:
raise commands.ClientException(f"{error}") from error