Repository: python/mypy

Issue:
Add static links for error codes
It would be great to provide a link with a bit more detailed explanation for every error code. For example:
```
main.py:10: error [bad_variance]: Invariant type variable 'T' used in protocol where covariant one is expected
main.py:10: note: See https://mypy-lang.org/error/bad_variance for more details
```
The the link will give some explanation why this is an error and links to relevant documentation section on Read the Docs. (Such notes may be shown only once for every error code.)

Something similar is used by SQLAlchemy (but their errors use numerical codes), see e.g. http://sqlalche.me/e/f405.

Depends on https://github.com/python/mypy/pull/6472


Discussion:
I want to work on this issue. can you tell me where are these errors messages.
@Shivansh-khare you can grep for them in source code, most of them (maybe all?) appear in `messages.py` I think.
@JelleZijlstra Can I hard code it with msg variable or add error_note_function for every error msg as done with incompatible_argument function at line:563 . 
As a first step we can create consistent links in the docs, see https://github.com/python/mypy/pull/15435

Patch:
diff --git a/mypy/errors.py b/mypy/errors.py
--- a/mypy/errors.py
+++ b/mypy/errors.py
@@ -20,8 +20,27 @@
 # 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"
+
 # 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}
@@ -107,6 +126,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
@@ -125,6 +145,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:
@@ -530,6 +551,35 @@ 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
+            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:
+                return
+            self.only_once_messages.add(message)
+            info = ErrorInfo(
+                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,
+                priority=20,
+            )
+            self._add_error_info(file, info)
 
     def has_many_errors(self) -> bool:
         if self.options.many_errors_threshold < 0:
@@ -1041,6 +1091,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/main.py b/mypy/main.py
--- a/mypy/main.py
+++ b/mypy/main.py
@@ -887,6 +887,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
--- a/mypy/options.py
+++ b/mypy/options.py
@@ -313,6 +313,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


Test patch:
diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test
--- a/test-data/unit/check-flags.test
+++ b/test-data/unit/check-flags.test
@@ -2195,3 +2195,18 @@ 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]
+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]