From ed905cdf1d9c91867839ed7f92a9b56c18e2483a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 15 Jun 2023 23:03:34 +0100 Subject: [PATCH 1/4] Add option to show links to error code docs (once per code) --- mypy/errors.py | 30 ++++++++++++++++++++++++++++++ mypy/main.py | 6 ++++++ mypy/options.py | 1 + test-data/unit/check-flags.test | 7 +++++++ 4 files changed, 44 insertions(+) diff --git a/mypy/errors.py b/mypy/errors.py index 9d29259e943c..34fbb8e25253 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -22,6 +22,8 @@ SHOW_NOTE_CODES: Final = {codes.ANNOTATION_UNCHECKED} allowed_duplicates: Final = ["@overload", "Got:", "Expected:"] +BASE_RTD_URL: Final = "https://mypy.rtfd.io/en/stable/_refs.html#code" + # Keep track of the original error code when the error code of a message is changed. # This is used to give notes about out-of-date "type: ignore" comments. original_error_codes: Final = {codes.LITERAL_REQ: codes.MISC, codes.TYPE_ABSTRACT: codes.MISC} @@ -434,6 +436,34 @@ def report( target=self.current_target(), ) self.add_error_info(info) + if ( + self.options.show_error_code_links + and not self.options.hide_error_codes + and code is not None + ): + message = f"See {BASE_RTD_URL}-{code.code} for information about this error" + if offset: + message = " " * offset + message + info = ErrorInfo( + self.import_context(), + file, + self.current_module(), + type, + function, + line, + column, + end_line, + end_column, + "note", + message, + code, + blocker=False, + only_once=True, + allow_dups=False, + origin=(self.file, origin_span), + target=self.current_target(), + ) + self.add_error_info(info) def _add_error_info(self, file: str, info: ErrorInfo) -> None: assert file not in self.flushed_files diff --git a/mypy/main.py b/mypy/main.py index 81a0a045745b..eb14391b64aa 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -885,6 +885,12 @@ def add_invertible_flag( help="Hide error codes in error messages", group=error_group, ) + add_invertible_flag( + "--show-error-code-links", + default=False, + help="Show links to error code documentation", + group=error_group, + ) add_invertible_flag( "--pretty", default=False, diff --git a/mypy/options.py b/mypy/options.py index 2785d2034c54..b02b6bc5b7aa 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -309,6 +309,7 @@ def __init__(self) -> None: self.show_column_numbers: bool = False self.show_error_end: bool = False self.hide_error_codes = False + self.show_error_code_links = False # Use soft word wrap and show trimmed source snippets with error location markers. self.pretty = False self.dump_graph = False diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 6ec0849146c0..fc3ddcae7bcd 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2195,3 +2195,10 @@ cb(lambda x: a) # OK fn = lambda x: a cb(fn) + +[case testShowErrorCodeLinks] +# flags: --show-error-codes --show-error-code-links + +x: int = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] \ + # N: See https://mypy.rtfd.io/en/stable/_refs.html#code-assignment for information about this error +y: int = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] From 179b553bbe8338ba5ff5d1a7b6c9e50756ddfd22 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 16 Jun 2023 23:03:33 +0100 Subject: [PATCH 2/4] Address CR; dogfood --- mypy/errors.py | 86 ++++++++++++++++++++++----------- mypy_self_check.ini | 1 + test-data/unit/check-flags.test | 8 ++- 3 files changed, 66 insertions(+), 29 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 34fbb8e25253..c904a9cc05bf 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -108,6 +108,7 @@ def __init__( allow_dups: bool, origin: tuple[str, Iterable[int]] | None = None, target: str | None = None, + priority: int = 0, ) -> None: self.import_ctx = import_ctx self.file = file @@ -126,6 +127,7 @@ def __init__( self.allow_dups = allow_dups self.origin = origin or (file, [line]) self.target = target + self.priority = priority # Type used internally to represent errors: @@ -436,34 +438,6 @@ def report( target=self.current_target(), ) self.add_error_info(info) - if ( - self.options.show_error_code_links - and not self.options.hide_error_codes - and code is not None - ): - message = f"See {BASE_RTD_URL}-{code.code} for information about this error" - if offset: - message = " " * offset + message - info = ErrorInfo( - self.import_context(), - file, - self.current_module(), - type, - function, - line, - column, - end_line, - end_column, - "note", - message, - code, - blocker=False, - only_once=True, - allow_dups=False, - origin=(self.file, origin_span), - target=self.current_target(), - ) - self.add_error_info(info) def _add_error_info(self, file: str, info: ErrorInfo) -> None: assert file not in self.flushed_files @@ -558,6 +532,34 @@ def add_error_info(self, info: ErrorInfo) -> None: allow_dups=False, ) self._add_error_info(file, note) + if ( + self.options.show_error_code_links + and not self.options.hide_error_codes + and info.code is not None + ): + message = f"See {BASE_RTD_URL}-{info.code.code} for more info" + if message in self.only_once_messages: + return + self.only_once_messages.add(message) + info = ErrorInfo( + info.import_ctx, + info.file, + info.module, + info.type, + info.function_or_member, + info.line, + info.column, + info.end_line, + info.end_column, + "note", + message, + info.code, + blocker=False, + only_once=True, + allow_dups=False, + priority=20, + ) + self._add_error_info(file, info) def has_many_errors(self) -> bool: if self.options.many_errors_threshold < 0: @@ -1069,6 +1071,34 @@ def sort_messages(self, errors: list[ErrorInfo]) -> list[ErrorInfo]: # Sort the errors specific to a file according to line number and column. a = sorted(errors[i0:i], key=lambda x: (x.line, x.column)) + a = self.sort_within_context(a) + result.extend(a) + return result + + def sort_within_context(self, errors: list[ErrorInfo]) -> list[ErrorInfo]: + """For the same location decide which messages to show first/last. + + Currently, we only compare within the same error code, to decide the + order of various additional notes. + """ + result = [] + i = 0 + while i < len(errors): + i0 = i + # Find neighbouring errors with the same position and error code. + while ( + i + 1 < len(errors) + and errors[i + 1].line == errors[i].line + and errors[i + 1].column == errors[i].column + and errors[i + 1].end_line == errors[i].end_line + and errors[i + 1].end_column == errors[i].end_column + and errors[i + 1].code == errors[i].code + ): + i += 1 + i += 1 + + # Sort the messages specific to a given error by priority. + a = sorted(errors[i0:i], key=lambda x: x.priority) result.extend(a) return result diff --git a/mypy_self_check.ini b/mypy_self_check.ini index d20fcd60a9cb..62083d144621 100644 --- a/mypy_self_check.ini +++ b/mypy_self_check.ini @@ -9,6 +9,7 @@ plugins = misc/proper_plugin.py python_version = 3.7 exclude = mypy/typeshed/|mypyc/test-data/|mypyc/lib-rt/ enable_error_code = ignore-without-code,redundant-expr +show_error_code_links = True [mypy-mypy.visitor] # See docstring for NodeVisitor for motivation. diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index fc3ddcae7bcd..592409573314 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2200,5 +2200,11 @@ cb(fn) # flags: --show-error-codes --show-error-code-links x: int = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] \ - # N: See https://mypy.rtfd.io/en/stable/_refs.html#code-assignment for information about this error + # N: See https://mypy.rtfd.io/en/stable/_refs.html#code-assignment for more info y: int = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] +list(1) # E: No overload variant of "list" matches argument type "int" [call-overload] \ + # N: Possible overload variants: \ + # N: def [T] __init__(self) -> List[T] \ + # N: def [T] __init__(self, x: Iterable[T]) -> List[T] \ + # N: See https://mypy.rtfd.io/en/stable/_refs.html#code-call-overload for more info +[builtins fixtures/list.pyi] From 9481758c5f8d3f51920f685a0636bec6db3ece26 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 28 Jun 2023 11:03:03 +0100 Subject: [PATCH 3/4] Hide docs links for trivial error codes --- mypy/errors.py | 18 ++++++++++++++++++ test-data/unit/check-flags.test | 8 +++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index c904a9cc05bf..5169efc73fcf 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -20,6 +20,23 @@ # Show error codes for some note-level messages (these usually appear alone # and not as a comment for a previous error-level message). SHOW_NOTE_CODES: Final = {codes.ANNOTATION_UNCHECKED} + +# Do not add notes with links to error code docs to errors with these codes. +# We can tweak this set as we get more experience about what is helpful and what is not. +HIDE_LINK_CODES: Final = { + # This is a generic error code, so it has no useful docs + codes.MISC, + # These are trivial and have some custom notes (e.g. for list being invariant) + codes.ASSIGNMENT, + codes.ARG_TYPE, + codes.RETURN_VALUE, + # Undefined name/attribute errors are self-explanatory + codes.ATTR_DEFINED, + codes.NAME_DEFINED, + # Overrides have a custom link to docs + codes.OVERRIDE, +} + allowed_duplicates: Final = ["@overload", "Got:", "Expected:"] BASE_RTD_URL: Final = "https://mypy.rtfd.io/en/stable/_refs.html#code" @@ -536,6 +553,7 @@ def add_error_info(self, info: ErrorInfo) -> None: self.options.show_error_code_links and not self.options.hide_error_codes and info.code is not None + and info.code not in HIDE_LINK_CODES ): message = f"See {BASE_RTD_URL}-{info.code.code} for more info" if message in self.only_once_messages: diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 592409573314..c356028f6620 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2199,12 +2199,14 @@ cb(fn) [case testShowErrorCodeLinks] # flags: --show-error-codes --show-error-code-links -x: int = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] \ - # N: See https://mypy.rtfd.io/en/stable/_refs.html#code-assignment for more info -y: int = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] +x: int = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] list(1) # E: No overload variant of "list" matches argument type "int" [call-overload] \ # N: Possible overload variants: \ # N: def [T] __init__(self) -> List[T] \ # N: def [T] __init__(self, x: Iterable[T]) -> List[T] \ # N: See https://mypy.rtfd.io/en/stable/_refs.html#code-call-overload for more info +list(2) # E: No overload variant of "list" matches argument type "int" [call-overload] \ + # N: Possible overload variants: \ + # N: def [T] __init__(self) -> List[T] \ + # N: def [T] __init__(self, x: Iterable[T]) -> List[T] [builtins fixtures/list.pyi] From 2e80a68cbc8ca3b1704be7e4c3f406f3913ade6d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 28 Jun 2023 11:17:02 +0100 Subject: [PATCH 4/4] Fix --- mypy/errors.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 8b02c1ff687f..6739d30f16a4 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -562,18 +562,18 @@ def add_error_info(self, info: ErrorInfo) -> None: return self.only_once_messages.add(message) info = ErrorInfo( - info.import_ctx, - info.file, - info.module, - info.type, - info.function_or_member, - info.line, - info.column, - info.end_line, - info.end_column, - "note", - message, - info.code, + import_ctx=info.import_ctx, + file=info.file, + module=info.module, + typ=info.type, + function_or_member=info.function_or_member, + line=info.line, + column=info.column, + end_line=info.end_line, + end_column=info.end_column, + severity="note", + message=message, + code=info.code, blocker=False, only_once=True, allow_dups=False,