-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Error format support, and JSON output option #11396
Changes from 20 commits
393820c
282bd28
ccda5b0
c849a77
fd2feab
b188001
51c1acc
9177dab
bc5ceac
9d29ab0
bd6d48d
ba8d17f
a2bc04d
35974e4
1e5ec91
723219f
2228c0a
33d81b0
63001ea
d27be7e
efe5c5d
1872ae6
3abc9cb
6c9ab11
627ed8e
e425cbe
47f1b07
e00ad4a
c1fb6a2
aafe3aa
fae3215
89ad1d3
7a3f736
6d46f75
e71a372
8cca203
79e16a8
8bf4890
5899f26
880b8f3
0aafadf
4cab249
7fe71c3
ad8f1d6
e2fd45e
4b03c5c
e0e6896
a0dc6d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import json | ||
from abc import ABC, abstractmethod | ||
from typing import TYPE_CHECKING | ||
|
||
if TYPE_CHECKING: | ||
from mypy.errors import MypyError | ||
|
||
|
||
class ErrorFormatter(ABC): | ||
"""Defines how errors are formatted before being printed.""" | ||
|
||
@abstractmethod | ||
def report_error(self, error: "MypyError") -> str: | ||
raise NotImplementedError | ||
|
||
|
||
class JSONFormatter(ErrorFormatter): | ||
def report_error(self, error: "MypyError") -> str: | ||
return json.dumps( | ||
{ | ||
"file": error.file_path, | ||
"line": error.line, | ||
"column": error.column, | ||
"message": error.message, | ||
"hint": error.hint, | ||
"code": None if error.errorcode is None else error.errorcode.code, | ||
} | ||
) | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
from typing_extensions import Final, Literal, TypeAlias as _TypeAlias | ||
|
||
from mypy import errorcodes as codes | ||
from mypy.error_formatter import ErrorFormatter | ||
from mypy.errorcodes import IMPORT, ErrorCode | ||
from mypy.message_registry import ErrorMessage | ||
from mypy.options import Options | ||
|
@@ -837,19 +838,29 @@ def format_messages( | |
a.append(" " * (DEFAULT_SOURCE_OFFSET + column) + marker) | ||
return a | ||
|
||
def file_messages(self, path: str) -> list[str]: | ||
def file_messages(self, path: str, formatter: Optional[ErrorFormatter] = None) -> list[str]: | ||
"""Return a string list of new error messages from a given file. | ||
|
||
Use a form suitable for displaying to the user. | ||
""" | ||
if path not in self.error_info_map: | ||
return [] | ||
|
||
error_info = self.error_info_map[path] | ||
if formatter is not None: | ||
error_info = [info for info in error_info if not info.hidden] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's not duplicate this and instead pass |
||
error_tuples = self.render_messages(self.sort_messages(error_info)) | ||
error_tuples = self.remove_duplicates(error_tuples) | ||
|
||
errors = create_errors(error_tuples) | ||
return [formatter.report_error(err) for err in errors] | ||
|
||
self.flushed_files.add(path) | ||
source_lines = None | ||
if self.options.pretty: | ||
assert self.read_source | ||
source_lines = self.read_source(path) | ||
return self.format_messages(self.error_info_map[path], source_lines) | ||
return self.format_messages(error_info, source_lines) | ||
|
||
def new_messages(self) -> list[str]: | ||
"""Return a string list of new error messages. | ||
|
@@ -1187,3 +1198,56 @@ def report_internal_error( | |
# Exit. The caller has nothing more to say. | ||
# We use exit code 2 to signal that this is no ordinary error. | ||
raise SystemExit(2) | ||
|
||
|
||
class MypyError: | ||
def __init__( | ||
self, | ||
file_path: str, | ||
line: int, | ||
column: int, | ||
message: str, | ||
hint: str, | ||
errorcode: Optional[ErrorCode], | ||
) -> None: | ||
self.file_path = file_path | ||
self.line = line | ||
self.column = column | ||
self.message = message | ||
self.hint = hint | ||
self.errorcode = errorcode | ||
|
||
|
||
# (file_path, line, column) | ||
_ErrorLocation = Tuple[str, int, int] | ||
|
||
|
||
def create_errors(error_tuples: list[ErrorTuple]) -> list[MypyError]: | ||
errors: list[MypyError] = [] | ||
latest_error_at_location: dict[_ErrorLocation, MypyError] = {} | ||
|
||
for error_tuple in error_tuples: | ||
file_path, line, column, _, _, severity, message, _, errorcode = error_tuple | ||
if file_path is None: | ||
continue | ||
|
||
assert severity in ("error", "note") | ||
if severity == "note": | ||
error_location = (file_path, line, column) | ||
error = latest_error_at_location.get(error_location) | ||
if error is None: | ||
# No error tuple found for this hint. Ignoring it | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not instead generate a MypyError with some field that lets us indicate that this is a note? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed. |
||
continue | ||
|
||
if error.hint == "": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we make hint into a list of strings instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's often that a single hint is wrapped into multiple lines. For internal representaiton we can keep it as a list of strings but for the user I think it makes most sense to display it as a single string. |
||
error.hint = message | ||
else: | ||
error.hint += "\n" + message | ||
|
||
else: | ||
error = MypyError(file_path, line, column, message, "", errorcode) | ||
errors.append(error) | ||
error_location = (file_path, line, column) | ||
latest_error_at_location[error_location] = error | ||
|
||
return errors |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -520,6 +520,10 @@ def add_invertible_flag( | |
stdout=stdout, | ||
) | ||
|
||
general_group.add_argument( | ||
"-O", "--output", metavar="FORMAT", help="Set a custom output format" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should have fixed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
) | ||
|
||
config_group = parser.add_argument_group( | ||
title="Config file", | ||
description="Use a config file instead of command line arguments. " | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -345,8 +345,9 @@ def __init__(self) -> None: | |
|
||
self.disable_bytearray_promotion = False | ||
self.disable_memoryview_promotion = False | ||
|
||
self.force_uppercase_builtins = False | ||
# Sets output format | ||
self.output = "" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would None be a better default? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup. |
||
|
||
def use_lowercase_names(self) -> bool: | ||
if self.python_version >= (3, 9): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This module should define a dictionary
str -> ErrorFormatter
that can be used inbuild.py
.