| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276 |
- from __future__ import annotations
- import os.path
- import sys
- import traceback
- from collections import defaultdict
- from typing import Callable, Final, Iterable, NoReturn, Optional, TextIO, Tuple, TypeVar
- from typing_extensions import Literal, TypeAlias as _TypeAlias
- from mypy import errorcodes as codes
- from mypy.errorcodes import IMPORT, ErrorCode
- from mypy.message_registry import ErrorMessage
- from mypy.options import Options
- from mypy.scope import Scope
- from mypy.util import DEFAULT_SOURCE_OFFSET, is_typeshed_file
- from mypy.version import __version__ as mypy_version
- T = TypeVar("T")
- # 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}
- class ErrorInfo:
- """Representation of a single error message."""
- # Description of a sequence of imports that refer to the source file
- # related to this error. Each item is a (path, line number) tuple.
- import_ctx: list[tuple[str, int]]
- # The path to source file that was the source of this error.
- file = ""
- # The fully-qualified id of the source module for this error.
- module: str | None = None
- # The name of the type in which this error is located at.
- type: str | None = "" # Unqualified, may be None
- # The name of the function or member in which this error is located at.
- function_or_member: str | None = "" # Unqualified, may be None
- # The line number related to this error within file.
- line = 0 # -1 if unknown
- # The column number related to this error with file.
- column = 0 # -1 if unknown
- # The end line number related to this error within file.
- end_line = 0 # -1 if unknown
- # The end column number related to this error with file.
- end_column = 0 # -1 if unknown
- # Either 'error' or 'note'
- severity = ""
- # The error message.
- message = ""
- # The error code.
- code: ErrorCode | None = None
- # If True, we should halt build after the file that generated this error.
- blocker = False
- # Only report this particular messages once per program.
- only_once = False
- # Do not remove duplicate copies of this message (ignored if only_once is True).
- allow_dups = False
- # Actual origin of the error message as tuple (path, line number, end line number)
- # If end line number is unknown, use line number.
- origin: tuple[str, Iterable[int]]
- # Fine-grained incremental target where this was reported
- target: str | None = None
- # If True, don't show this message in output, but still record the error (needed
- # by mypy daemon)
- hidden = False
- def __init__(
- self,
- import_ctx: list[tuple[str, int]],
- *,
- file: str,
- module: str | None,
- typ: str | None,
- function_or_member: str | None,
- line: int,
- column: int,
- end_line: int,
- end_column: int,
- severity: str,
- message: str,
- code: ErrorCode | None,
- blocker: bool,
- only_once: bool,
- 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
- self.module = module
- self.type = typ
- self.function_or_member = function_or_member
- self.line = line
- self.column = column
- self.end_line = end_line
- self.end_column = end_column
- self.severity = severity
- self.message = message
- self.code = code
- self.blocker = blocker
- self.only_once = only_once
- self.allow_dups = allow_dups
- self.origin = origin or (file, [line])
- self.target = target
- self.priority = priority
- # Type used internally to represent errors:
- # (path, line, column, end_line, end_column, severity, message, allow_dups, code)
- ErrorTuple: _TypeAlias = Tuple[
- Optional[str], int, int, int, int, str, str, bool, Optional[ErrorCode]
- ]
- class ErrorWatcher:
- """Context manager that can be used to keep track of new errors recorded
- around a given operation.
- Errors maintain a stack of such watchers. The handler is called starting
- at the top of the stack, and is propagated down the stack unless filtered
- out by one of the ErrorWatcher instances.
- """
- def __init__(
- self,
- errors: Errors,
- *,
- filter_errors: bool | Callable[[str, ErrorInfo], bool] = False,
- save_filtered_errors: bool = False,
- ):
- self.errors = errors
- self._has_new_errors = False
- self._filter = filter_errors
- self._filtered: list[ErrorInfo] | None = [] if save_filtered_errors else None
- def __enter__(self) -> ErrorWatcher:
- self.errors._watchers.append(self)
- return self
- def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> Literal[False]:
- last = self.errors._watchers.pop()
- assert last == self
- return False
- def on_error(self, file: str, info: ErrorInfo) -> bool:
- """Handler called when a new error is recorded.
- The default implementation just sets the has_new_errors flag
- Return True to filter out the error, preventing it from being seen by other
- ErrorWatcher further down the stack and from being recorded by Errors
- """
- self._has_new_errors = True
- if isinstance(self._filter, bool):
- should_filter = self._filter
- elif callable(self._filter):
- should_filter = self._filter(file, info)
- else:
- raise AssertionError(f"invalid error filter: {type(self._filter)}")
- if should_filter and self._filtered is not None:
- self._filtered.append(info)
- return should_filter
- def has_new_errors(self) -> bool:
- return self._has_new_errors
- def filtered_errors(self) -> list[ErrorInfo]:
- assert self._filtered is not None
- return self._filtered
- class Errors:
- """Container for compile errors.
- This class generates and keeps tracks of compile errors and the
- current error context (nested imports).
- """
- # Map from files to generated error messages. Is an OrderedDict so
- # that it can be used to order messages based on the order the
- # files were processed.
- error_info_map: dict[str, list[ErrorInfo]]
- # optimization for legacy codebases with many files with errors
- has_blockers: set[str]
- # Files that we have reported the errors for
- flushed_files: set[str]
- # Current error context: nested import context/stack, as a list of (path, line) pairs.
- import_ctx: list[tuple[str, int]]
- # Path name prefix that is removed from all paths, if set.
- ignore_prefix: str | None = None
- # Path to current file.
- file: str = ""
- # Ignore some errors on these lines of each file
- # (path -> line -> error-codes)
- ignored_lines: dict[str, dict[int, list[str]]]
- # Lines that were skipped during semantic analysis e.g. due to ALWAYS_FALSE, MYPY_FALSE,
- # or platform/version checks. Those lines would not be type-checked.
- skipped_lines: dict[str, set[int]]
- # Lines on which an error was actually ignored.
- used_ignored_lines: dict[str, dict[int, list[str]]]
- # Files where all errors should be ignored.
- ignored_files: set[str]
- # Collection of reported only_once messages.
- only_once_messages: set[str]
- # Set to True to show "In function "foo":" messages.
- show_error_context: bool = False
- # Set to True to show column numbers in error messages.
- show_column_numbers: bool = False
- # Set to True to show end line and end column in error messages.
- # Ths implies `show_column_numbers`.
- show_error_end: bool = False
- # Set to True to show absolute file paths in error messages.
- show_absolute_path: bool = False
- # State for keeping track of the current fine-grained incremental mode target.
- # (See mypy.server.update for more about targets.)
- # Current module id.
- target_module: str | None = None
- scope: Scope | None = None
- # Have we seen an import-related error so far? If yes, we filter out other messages
- # in some cases to avoid reporting huge numbers of errors.
- seen_import_error = False
- _watchers: list[ErrorWatcher] = []
- def __init__(
- self,
- options: Options,
- *,
- read_source: Callable[[str], list[str] | None] | None = None,
- hide_error_codes: bool | None = None,
- ) -> None:
- self.options = options
- self.hide_error_codes = (
- hide_error_codes if hide_error_codes is not None else options.hide_error_codes
- )
- # We use fscache to read source code when showing snippets.
- self.read_source = read_source
- self.initialize()
- def initialize(self) -> None:
- self.error_info_map = {}
- self.flushed_files = set()
- self.import_ctx = []
- self.function_or_member = [None]
- self.ignored_lines = {}
- self.skipped_lines = {}
- self.used_ignored_lines = defaultdict(lambda: defaultdict(list))
- self.ignored_files = set()
- self.only_once_messages = set()
- self.has_blockers = set()
- self.scope = None
- self.target_module = None
- self.seen_import_error = False
- def reset(self) -> None:
- self.initialize()
- def set_ignore_prefix(self, prefix: str) -> None:
- """Set path prefix that will be removed from all paths."""
- prefix = os.path.normpath(prefix)
- # Add separator to the end, if not given.
- if os.path.basename(prefix) != "":
- prefix += os.sep
- self.ignore_prefix = prefix
- def simplify_path(self, file: str) -> str:
- if self.options.show_absolute_path:
- return os.path.abspath(file)
- else:
- file = os.path.normpath(file)
- return remove_path_prefix(file, self.ignore_prefix)
- def set_file(
- self, file: str, module: str | None, options: Options, scope: Scope | None = None
- ) -> None:
- """Set the path and module id of the current file."""
- # The path will be simplified later, in render_messages. That way
- # * 'file' is always a key that uniquely identifies a source file
- # that mypy read (simplified paths might not be unique); and
- # * we only have to simplify in one place, while still supporting
- # reporting errors for files other than the one currently being
- # processed.
- self.file = file
- self.target_module = module
- self.scope = scope
- self.options = options
- def set_file_ignored_lines(
- self, file: str, ignored_lines: dict[int, list[str]], ignore_all: bool = False
- ) -> None:
- self.ignored_lines[file] = ignored_lines
- if ignore_all:
- self.ignored_files.add(file)
- def set_skipped_lines(self, file: str, skipped_lines: set[int]) -> None:
- self.skipped_lines[file] = skipped_lines
- def current_target(self) -> str | None:
- """Retrieves the current target from the associated scope.
- If there is no associated scope, use the target module."""
- if self.scope is not None:
- return self.scope.current_target()
- return self.target_module
- def current_module(self) -> str | None:
- return self.target_module
- def import_context(self) -> list[tuple[str, int]]:
- """Return a copy of the import context."""
- return self.import_ctx.copy()
- def set_import_context(self, ctx: list[tuple[str, int]]) -> None:
- """Replace the entire import context with a new value."""
- self.import_ctx = ctx.copy()
- def report(
- self,
- line: int,
- column: int | None,
- message: str,
- code: ErrorCode | None = None,
- *,
- blocker: bool = False,
- severity: str = "error",
- file: str | None = None,
- only_once: bool = False,
- allow_dups: bool = False,
- origin_span: Iterable[int] | None = None,
- offset: int = 0,
- end_line: int | None = None,
- end_column: int | None = None,
- ) -> None:
- """Report message at the given line using the current error context.
- Args:
- line: line number of error
- column: column number of error
- message: message to report
- code: error code (defaults to 'misc'; not shown for notes)
- blocker: if True, don't continue analysis after this error
- severity: 'error' or 'note'
- file: if non-None, override current file as context
- only_once: if True, only report this exact message once per build
- allow_dups: if True, allow duplicate copies of this message (ignored if only_once)
- origin_span: if non-None, override current context as origin
- (type: ignores have effect here)
- end_line: if non-None, override current context as end
- """
- if self.scope:
- type = self.scope.current_type_name()
- if self.scope.ignored > 0:
- type = None # Omit type context if nested function
- function = self.scope.current_function_name()
- else:
- type = None
- function = None
- if column is None:
- column = -1
- if end_column is None:
- if column == -1:
- end_column = -1
- else:
- end_column = column + 1
- if file is None:
- file = self.file
- if offset:
- message = " " * offset + message
- if origin_span is None:
- origin_span = [line]
- if end_line is None:
- end_line = line
- code = code or (codes.MISC if not blocker else None)
- info = ErrorInfo(
- import_ctx=self.import_context(),
- file=file,
- module=self.current_module(),
- typ=type,
- function_or_member=function,
- line=line,
- column=column,
- end_line=end_line,
- end_column=end_column,
- severity=severity,
- message=message,
- code=code,
- blocker=blocker,
- only_once=only_once,
- allow_dups=allow_dups,
- 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
- # process the stack of ErrorWatchers before modifying any internal state
- # in case we need to filter out the error entirely
- if self._filter_error(file, info):
- return
- if file not in self.error_info_map:
- self.error_info_map[file] = []
- self.error_info_map[file].append(info)
- if info.blocker:
- self.has_blockers.add(file)
- if info.code is IMPORT:
- self.seen_import_error = True
- def _filter_error(self, file: str, info: ErrorInfo) -> bool:
- """
- process ErrorWatcher stack from top to bottom,
- stopping early if error needs to be filtered out
- """
- i = len(self._watchers)
- while i > 0:
- i -= 1
- w = self._watchers[i]
- if w.on_error(file, info):
- return True
- return False
- def add_error_info(self, info: ErrorInfo) -> None:
- file, lines = info.origin
- # process the stack of ErrorWatchers before modifying any internal state
- # in case we need to filter out the error entirely
- # NB: we need to do this both here and in _add_error_info, otherwise we
- # might incorrectly update the sets of ignored or only_once messages
- if self._filter_error(file, info):
- return
- if not info.blocker: # Blockers cannot be ignored
- if file in self.ignored_lines:
- # Check each line in this context for "type: ignore" comments.
- # line == end_line for most nodes, so we only loop once.
- for scope_line in lines:
- if self.is_ignored_error(scope_line, info, self.ignored_lines[file]):
- # Annotation requests us to ignore all errors on this line.
- self.used_ignored_lines[file][scope_line].append(
- (info.code or codes.MISC).code
- )
- return
- if file in self.ignored_files:
- return
- if info.only_once:
- if info.message in self.only_once_messages:
- return
- self.only_once_messages.add(info.message)
- if self.seen_import_error and info.code is not IMPORT and self.has_many_errors():
- # Missing stubs can easily cause thousands of errors about
- # Any types, especially when upgrading to mypy 0.900,
- # which no longer bundles third-party library stubs. Avoid
- # showing too many errors to make it easier to see
- # import-related errors.
- info.hidden = True
- self.report_hidden_errors(info)
- self._add_error_info(file, info)
- ignored_codes = self.ignored_lines.get(file, {}).get(info.line, [])
- if ignored_codes and info.code:
- # Something is ignored on the line, but not this error, so maybe the error
- # code is incorrect.
- msg = f'Error code "{info.code.code}" not covered by "type: ignore" comment'
- if info.code in original_error_codes:
- # If there seems to be a "type: ignore" with a stale error
- # code, report a more specific note.
- old_code = original_error_codes[info.code].code
- if old_code in ignored_codes:
- msg = (
- f'Error code changed to {info.code.code}; "type: ignore" comment '
- + "may be out of date"
- )
- note = 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=msg,
- code=None,
- blocker=False,
- only_once=False,
- 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:
- return False
- if len(self.error_info_map) >= self.options.many_errors_threshold:
- return True
- if (
- sum(len(errors) for errors in self.error_info_map.values())
- >= self.options.many_errors_threshold
- ):
- return True
- return False
- def report_hidden_errors(self, info: ErrorInfo) -> None:
- message = (
- "(Skipping most remaining errors due to unresolved imports or missing stubs; "
- + "fix these first)"
- )
- if message in self.only_once_messages:
- return
- self.only_once_messages.add(message)
- new_info = ErrorInfo(
- import_ctx=info.import_ctx,
- file=info.file,
- module=info.module,
- typ=None,
- function_or_member=None,
- line=info.line,
- column=info.column,
- end_line=info.end_line,
- end_column=info.end_column,
- severity="note",
- message=message,
- code=None,
- blocker=False,
- only_once=True,
- allow_dups=False,
- origin=info.origin,
- target=info.target,
- )
- self._add_error_info(info.origin[0], new_info)
- def is_ignored_error(self, line: int, info: ErrorInfo, ignores: dict[int, list[str]]) -> bool:
- if info.blocker:
- # Blocking errors can never be ignored
- return False
- if info.code and not self.is_error_code_enabled(info.code):
- return True
- if line not in ignores:
- return False
- if not ignores[line]:
- # Empty list means that we ignore all errors
- return True
- if info.code and self.is_error_code_enabled(info.code):
- return (
- info.code.code in ignores[line]
- or info.code.sub_code_of is not None
- and info.code.sub_code_of.code in ignores[line]
- )
- return False
- def is_error_code_enabled(self, error_code: ErrorCode) -> bool:
- if self.options:
- current_mod_disabled = self.options.disabled_error_codes
- current_mod_enabled = self.options.enabled_error_codes
- else:
- current_mod_disabled = set()
- current_mod_enabled = set()
- if error_code in current_mod_disabled:
- return False
- elif error_code in current_mod_enabled:
- return True
- elif error_code.sub_code_of is not None and error_code.sub_code_of in current_mod_disabled:
- return False
- else:
- return error_code.default_enabled
- def clear_errors_in_targets(self, path: str, targets: set[str]) -> None:
- """Remove errors in specific fine-grained targets within a file."""
- if path in self.error_info_map:
- new_errors = []
- has_blocker = False
- for info in self.error_info_map[path]:
- if info.target not in targets:
- new_errors.append(info)
- has_blocker |= info.blocker
- elif info.only_once:
- self.only_once_messages.remove(info.message)
- self.error_info_map[path] = new_errors
- if not has_blocker and path in self.has_blockers:
- self.has_blockers.remove(path)
- def generate_unused_ignore_errors(self, file: str) -> None:
- if (
- is_typeshed_file(self.options.abs_custom_typeshed_dir if self.options else None, file)
- or file in self.ignored_files
- ):
- return
- ignored_lines = self.ignored_lines[file]
- used_ignored_lines = self.used_ignored_lines[file]
- for line, ignored_codes in ignored_lines.items():
- if line in self.skipped_lines[file]:
- continue
- if codes.UNUSED_IGNORE.code in ignored_codes:
- continue
- used_ignored_codes = used_ignored_lines[line]
- unused_ignored_codes = set(ignored_codes) - set(used_ignored_codes)
- # `ignore` is used
- if not ignored_codes and used_ignored_codes:
- continue
- # All codes appearing in `ignore[...]` are used
- if ignored_codes and not unused_ignored_codes:
- continue
- # Display detail only when `ignore[...]` specifies more than one error code
- unused_codes_message = ""
- if len(ignored_codes) > 1 and unused_ignored_codes:
- unused_codes_message = f"[{', '.join(sorted(unused_ignored_codes))}]"
- message = f'Unused "type: ignore{unused_codes_message}" comment'
- for unused in unused_ignored_codes:
- narrower = set(used_ignored_codes) & codes.sub_code_map[unused]
- if narrower:
- message += f", use narrower [{', '.join(narrower)}] instead of [{unused}] code"
- # Don't use report since add_error_info will ignore the error!
- info = ErrorInfo(
- import_ctx=self.import_context(),
- file=file,
- module=self.current_module(),
- typ=None,
- function_or_member=None,
- line=line,
- column=-1,
- end_line=line,
- end_column=-1,
- severity="error",
- message=message,
- code=codes.UNUSED_IGNORE,
- blocker=False,
- only_once=False,
- allow_dups=False,
- )
- self._add_error_info(file, info)
- def generate_ignore_without_code_errors(
- self, file: str, is_warning_unused_ignores: bool
- ) -> None:
- if (
- is_typeshed_file(self.options.abs_custom_typeshed_dir if self.options else None, file)
- or file in self.ignored_files
- ):
- return
- used_ignored_lines = self.used_ignored_lines[file]
- # If the whole file is ignored, ignore it.
- if used_ignored_lines:
- _, used_codes = min(used_ignored_lines.items())
- if codes.FILE.code in used_codes:
- return
- for line, ignored_codes in self.ignored_lines[file].items():
- if ignored_codes:
- continue
- # If the ignore is itself unused and that would be warned about, let
- # that error stand alone
- if is_warning_unused_ignores and not used_ignored_lines[line]:
- continue
- codes_hint = ""
- ignored_codes = sorted(set(used_ignored_lines[line]))
- if ignored_codes:
- codes_hint = f' (consider "type: ignore[{", ".join(ignored_codes)}]" instead)'
- message = f'"type: ignore" comment without error code{codes_hint}'
- # Don't use report since add_error_info will ignore the error!
- info = ErrorInfo(
- import_ctx=self.import_context(),
- file=file,
- module=self.current_module(),
- typ=None,
- function_or_member=None,
- line=line,
- column=-1,
- end_line=line,
- end_column=-1,
- severity="error",
- message=message,
- code=codes.IGNORE_WITHOUT_CODE,
- blocker=False,
- only_once=False,
- allow_dups=False,
- )
- self._add_error_info(file, info)
- def num_messages(self) -> int:
- """Return the number of generated messages."""
- return sum(len(x) for x in self.error_info_map.values())
- def is_errors(self) -> bool:
- """Are there any generated messages?"""
- return bool(self.error_info_map)
- def is_blockers(self) -> bool:
- """Are the any errors that are blockers?"""
- return bool(self.has_blockers)
- def blocker_module(self) -> str | None:
- """Return the module with a blocking error, or None if not possible."""
- for path in self.has_blockers:
- for err in self.error_info_map[path]:
- if err.blocker:
- return err.module
- return None
- def is_errors_for_file(self, file: str) -> bool:
- """Are there any errors for the given file?"""
- return file in self.error_info_map
- def prefer_simple_messages(self) -> bool:
- """Should we generate simple/fast error messages?
- Return True if errors are not shown to user, i.e. errors are ignored
- or they are collected for internal use only.
- If True, we should prefer to generate a simple message quickly.
- All normal errors should still be reported.
- """
- if self.file in self.ignored_files:
- # Errors ignored, so no point generating fancy messages
- return True
- for _watcher in self._watchers:
- if _watcher._filter is True and _watcher._filtered is None:
- # Errors are filtered
- return True
- return False
- def raise_error(self, use_stdout: bool = True) -> NoReturn:
- """Raise a CompileError with the generated messages.
- Render the messages suitable for displaying.
- """
- # self.new_messages() will format all messages that haven't already
- # been returned from a file_messages() call.
- raise CompileError(
- self.new_messages(), use_stdout=use_stdout, module_with_blocker=self.blocker_module()
- )
- def format_messages(
- self, error_info: list[ErrorInfo], source_lines: list[str] | None
- ) -> list[str]:
- """Return a string list that represents the error messages.
- Use a form suitable for displaying to the user. If self.pretty
- is True also append a relevant trimmed source code line (only for
- severity 'error').
- """
- a: list[str] = []
- error_info = [info for info in error_info if not info.hidden]
- errors = self.render_messages(self.sort_messages(error_info))
- errors = self.remove_duplicates(errors)
- for (
- file,
- line,
- column,
- end_line,
- end_column,
- severity,
- message,
- allow_dups,
- code,
- ) in errors:
- s = ""
- if file is not None:
- if self.options.show_column_numbers and line >= 0 and column >= 0:
- srcloc = f"{file}:{line}:{1 + column}"
- if self.options.show_error_end and end_line >= 0 and end_column >= 0:
- srcloc += f":{end_line}:{end_column}"
- elif line >= 0:
- srcloc = f"{file}:{line}"
- else:
- srcloc = file
- s = f"{srcloc}: {severity}: {message}"
- else:
- s = message
- if (
- not self.hide_error_codes
- and code
- and (severity != "note" or code in SHOW_NOTE_CODES)
- ):
- # If note has an error code, it is related to a previous error. Avoid
- # displaying duplicate error codes.
- s = f"{s} [{code.code}]"
- a.append(s)
- if self.options.pretty:
- # Add source code fragment and a location marker.
- if severity == "error" and source_lines and line > 0:
- source_line = source_lines[line - 1]
- source_line_expanded = source_line.expandtabs()
- if column < 0:
- # Something went wrong, take first non-empty column.
- column = len(source_line) - len(source_line.lstrip())
- # Shifts column after tab expansion
- column = len(source_line[:column].expandtabs())
- end_column = len(source_line[:end_column].expandtabs())
- # Note, currently coloring uses the offset to detect source snippets,
- # so these offsets should not be arbitrary.
- a.append(" " * DEFAULT_SOURCE_OFFSET + source_line_expanded)
- marker = "^"
- if end_line == line and end_column > column:
- marker = f'^{"~" * (end_column - column - 1)}'
- a.append(" " * (DEFAULT_SOURCE_OFFSET + column) + marker)
- return a
- def file_messages(self, path: str) -> 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 []
- 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)
- def new_messages(self) -> list[str]:
- """Return a string list of new error messages.
- Use a form suitable for displaying to the user.
- Errors from different files are ordered based on the order in which
- they first generated an error.
- """
- msgs = []
- for path in self.error_info_map.keys():
- if path not in self.flushed_files:
- msgs.extend(self.file_messages(path))
- return msgs
- def targets(self) -> set[str]:
- """Return a set of all targets that contain errors."""
- # TODO: Make sure that either target is always defined or that not being defined
- # is okay for fine-grained incremental checking.
- return {
- info.target for errs in self.error_info_map.values() for info in errs if info.target
- }
- def render_messages(self, errors: list[ErrorInfo]) -> list[ErrorTuple]:
- """Translate the messages into a sequence of tuples.
- Each tuple is of form (path, line, col, severity, message, allow_dups, code).
- The rendered sequence includes information about error contexts.
- The path item may be None. If the line item is negative, the
- line number is not defined for the tuple.
- """
- result: list[ErrorTuple] = []
- prev_import_context: list[tuple[str, int]] = []
- prev_function_or_member: str | None = None
- prev_type: str | None = None
- for e in errors:
- # Report module import context, if different from previous message.
- if not self.options.show_error_context:
- pass
- elif e.import_ctx != prev_import_context:
- last = len(e.import_ctx) - 1
- i = last
- while i >= 0:
- path, line = e.import_ctx[i]
- fmt = "{}:{}: note: In module imported here"
- if i < last:
- fmt = "{}:{}: note: ... from here"
- if i > 0:
- fmt += ","
- else:
- fmt += ":"
- # Remove prefix to ignore from path (if present) to
- # simplify path.
- path = remove_path_prefix(path, self.ignore_prefix)
- result.append(
- (None, -1, -1, -1, -1, "note", fmt.format(path, line), e.allow_dups, None)
- )
- i -= 1
- file = self.simplify_path(e.file)
- # Report context within a source file.
- if not self.options.show_error_context:
- pass
- elif e.function_or_member != prev_function_or_member or e.type != prev_type:
- if e.function_or_member is None:
- if e.type is None:
- result.append(
- (file, -1, -1, -1, -1, "note", "At top level:", e.allow_dups, None)
- )
- else:
- result.append(
- (
- file,
- -1,
- -1,
- -1,
- -1,
- "note",
- f'In class "{e.type}":',
- e.allow_dups,
- None,
- )
- )
- else:
- if e.type is None:
- result.append(
- (
- file,
- -1,
- -1,
- -1,
- -1,
- "note",
- f'In function "{e.function_or_member}":',
- e.allow_dups,
- None,
- )
- )
- else:
- result.append(
- (
- file,
- -1,
- -1,
- -1,
- -1,
- "note",
- 'In member "{}" of class "{}":'.format(
- e.function_or_member, e.type
- ),
- e.allow_dups,
- None,
- )
- )
- elif e.type != prev_type:
- if e.type is None:
- result.append(
- (file, -1, -1, -1, -1, "note", "At top level:", e.allow_dups, None)
- )
- else:
- result.append(
- (file, -1, -1, -1, -1, "note", f'In class "{e.type}":', e.allow_dups, None)
- )
- if isinstance(e.message, ErrorMessage):
- result.append(
- (
- file,
- e.line,
- e.column,
- e.end_line,
- e.end_column,
- e.severity,
- e.message.value,
- e.allow_dups,
- e.code,
- )
- )
- else:
- result.append(
- (
- file,
- e.line,
- e.column,
- e.end_line,
- e.end_column,
- e.severity,
- e.message,
- e.allow_dups,
- e.code,
- )
- )
- prev_import_context = e.import_ctx
- prev_function_or_member = e.function_or_member
- prev_type = e.type
- return result
- def sort_messages(self, errors: list[ErrorInfo]) -> list[ErrorInfo]:
- """Sort an array of error messages locally by line number.
- I.e., sort a run of consecutive messages with the same
- context by line number, but otherwise retain the general
- ordering of the messages.
- """
- result: list[ErrorInfo] = []
- i = 0
- while i < len(errors):
- i0 = i
- # Find neighbouring errors with the same context and file.
- while (
- i + 1 < len(errors)
- and errors[i + 1].import_ctx == errors[i].import_ctx
- and errors[i + 1].file == errors[i].file
- ):
- i += 1
- i += 1
- # 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
- def remove_duplicates(self, errors: list[ErrorTuple]) -> list[ErrorTuple]:
- """Remove duplicates from a sorted error list."""
- res: list[ErrorTuple] = []
- i = 0
- while i < len(errors):
- dup = False
- # Use slightly special formatting for member conflicts reporting.
- conflicts_notes = False
- j = i - 1
- # Find duplicates, unless duplicates are allowed.
- if not errors[i][7]:
- while j >= 0 and errors[j][0] == errors[i][0]:
- if errors[j][6].strip() == "Got:":
- conflicts_notes = True
- j -= 1
- j = i - 1
- while j >= 0 and errors[j][0] == errors[i][0] and errors[j][1] == errors[i][1]:
- if (
- errors[j][5] == errors[i][5]
- and
- # Allow duplicate notes in overload conflicts reporting.
- not (
- (errors[i][5] == "note" and errors[i][6].strip() in allowed_duplicates)
- or (errors[i][6].strip().startswith("def ") and conflicts_notes)
- )
- and errors[j][6] == errors[i][6]
- ): # ignore column
- dup = True
- break
- j -= 1
- if not dup:
- res.append(errors[i])
- i += 1
- return res
- class CompileError(Exception):
- """Exception raised when there is a compile error.
- It can be a parse, semantic analysis, type check or other
- compilation-related error.
- CompileErrors raised from an errors object carry all of the
- messages that have not been reported out by error streaming.
- This is patched up by build.build to contain either all error
- messages (if errors were streamed) or none (if they were not).
- """
- messages: list[str]
- use_stdout = False
- # Can be set in case there was a module with a blocking error
- module_with_blocker: str | None = None
- def __init__(
- self, messages: list[str], use_stdout: bool = False, module_with_blocker: str | None = None
- ) -> None:
- super().__init__("\n".join(messages))
- self.messages = messages
- self.use_stdout = use_stdout
- self.module_with_blocker = module_with_blocker
- def remove_path_prefix(path: str, prefix: str | None) -> str:
- """If path starts with prefix, return copy of path with the prefix removed.
- Otherwise, return path. If path is None, return None.
- """
- if prefix is not None and path.startswith(prefix):
- return path[len(prefix) :]
- else:
- return path
- def report_internal_error(
- err: Exception,
- file: str | None,
- line: int,
- errors: Errors,
- options: Options,
- stdout: TextIO | None = None,
- stderr: TextIO | None = None,
- ) -> NoReturn:
- """Report internal error and exit.
- This optionally starts pdb or shows a traceback.
- """
- stdout = stdout or sys.stdout
- stderr = stderr or sys.stderr
- # Dump out errors so far, they often provide a clue.
- # But catch unexpected errors rendering them.
- try:
- for msg in errors.new_messages():
- print(msg)
- except Exception as e:
- print("Failed to dump errors:", repr(e), file=stderr)
- # Compute file:line prefix for official-looking error messages.
- if file:
- if line:
- prefix = f"{file}:{line}: "
- else:
- prefix = f"{file}: "
- else:
- prefix = ""
- # Print "INTERNAL ERROR" message.
- print(
- f"{prefix}error: INTERNAL ERROR --",
- "Please try using mypy master on GitHub:\n"
- "https://mypy.readthedocs.io/en/stable/common_issues.html"
- "#using-a-development-mypy-build",
- file=stderr,
- )
- if options.show_traceback:
- print("Please report a bug at https://github.com/python/mypy/issues", file=stderr)
- else:
- print(
- "If this issue continues with mypy master, "
- "please report a bug at https://github.com/python/mypy/issues",
- file=stderr,
- )
- print(f"version: {mypy_version}", file=stderr)
- # If requested, drop into pdb. This overrides show_tb.
- if options.pdb:
- print("Dropping into pdb", file=stderr)
- import pdb
- pdb.post_mortem(sys.exc_info()[2])
- # If requested, print traceback, else print note explaining how to get one.
- if options.raise_exceptions:
- raise err
- if not options.show_traceback:
- if not options.pdb:
- print(
- "{}: note: please use --show-traceback to print a traceback "
- "when reporting a bug".format(prefix),
- file=stderr,
- )
- else:
- tb = traceback.extract_stack()[:-2]
- tb2 = traceback.extract_tb(sys.exc_info()[2])
- print("Traceback (most recent call last):")
- for s in traceback.format_list(tb + tb2):
- print(s.rstrip("\n"))
- print(f"{type(err).__name__}: {err}", file=stdout)
- print(f"{prefix}: note: use --pdb to drop into pdb", file=stderr)
- # Exit. The caller has nothing more to say.
- # We use exit code 2 to signal that this is no ordinary error.
- raise SystemExit(2)
|