blob: 373f6e7ad757514b4e75f077ce0e0e579d7e8678 [file] [log] [blame]
import os.path
import sys
import traceback
from collections import OrderedDict, defaultdict
from typing import Tuple, List, TypeVar, Set, Dict
from mypy.options import Options
T = TypeVar('T')
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 = None # type: List[Tuple[str, int]]
# The source file that was the source of this error.
file = ''
# The name of the type in which this error is located at.
type = '' # Unqualified, may be None
# The name of the function or member in which this error is located at.
function_or_member = '' # 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
# Either 'error' or 'note'.
severity = ''
# The error message.
message = ''
# 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
def __init__(self, import_ctx: List[Tuple[str, int]], file: str, typ: str,
function_or_member: str, line: int, column: int, severity: str,
message: str, blocker: bool, only_once: bool) -> None:
self.import_ctx = import_ctx
self.file = file
self.type = typ
self.function_or_member = function_or_member
self.line = line
self.column = column
self.severity = severity
self.message = message
self.blocker = blocker
self.only_once = only_once
class Errors:
"""Container for compile errors.
This class generates and keeps tracks of compile errors and the
current error context (nested imports).
"""
# List of generated error messages.
error_info = None # type: List[ErrorInfo]
# Current error context: nested import context/stack, as a list of (path, line) pairs.
import_ctx = None # type: List[Tuple[str, int]]
# Path name prefix that is removed from all paths, if set.
ignore_prefix = None # type: str
# Path to current file.
file = None # type: str
# Stack of short names of currents types (or None).
type_name = None # type: List[str]
# Stack of short names of current functions or members (or None).
function_or_member = None # type: List[str]
# Ignore errors on these lines of each file.
ignored_lines = None # type: Dict[str, Set[int]]
# Lines on which an error was actually ignored.
used_ignored_lines = None # type: Dict[str, Set[int]]
# Collection of reported only_once messages.
only_once_messages = None # type: Set[str]
# Set to True to suppress "In function "foo":" messages.
hide_error_context = False # type: bool
# Set to True to show column numbers in error messages
show_column_numbers = False # type: bool
def __init__(self, hide_error_context: bool = False,
show_column_numbers: bool = False) -> None:
self.error_info = []
self.import_ctx = []
self.type_name = [None]
self.function_or_member = [None]
self.ignored_lines = OrderedDict()
self.used_ignored_lines = defaultdict(set)
self.only_once_messages = set()
self.hide_error_context = hide_error_context
self.show_column_numbers = show_column_numbers
def copy(self) -> 'Errors':
new = Errors(self.hide_error_context, self.show_column_numbers)
new.file = self.file
new.import_ctx = self.import_ctx[:]
new.type_name = self.type_name[:]
new.function_or_member = self.function_or_member[:]
return new
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:
file = os.path.normpath(file)
return remove_path_prefix(file, self.ignore_prefix)
def set_file(self, file: str, ignored_lines: Set[int] = None) -> None:
"""Set the path 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
def set_file_ignored_lines(self, file: str, ignored_lines: Set[int] = None) -> None:
self.ignored_lines[file] = ignored_lines
def push_function(self, name: str) -> None:
"""Set the current function or member short name (it can be None)."""
self.function_or_member.append(name)
def pop_function(self) -> None:
self.function_or_member.pop()
def push_type(self, name: str) -> None:
"""Set the short name of the current type (it can be None)."""
self.type_name.append(name)
def pop_type(self) -> None:
self.type_name.pop()
def import_context(self) -> List[Tuple[str, int]]:
"""Return a copy of the import context."""
return self.import_ctx[:]
def set_import_context(self, ctx: List[Tuple[str, int]]) -> None:
"""Replace the entire import context with a new value."""
self.import_ctx = ctx[:]
def report(self, line: int, column: int, message: str, blocker: bool = False,
severity: str = 'error', file: str = None, only_once: bool = False) -> None:
"""Report message at the given line using the current error context.
Args:
line: line number of error
message: message to report
blocker: if True, don't continue analysis after this error
severity: 'error', 'note' or 'warning'
file: if non-None, override current file as context
only_once: if True, only report this exact message once per build
"""
type = self.type_name[-1]
if len(self.function_or_member) > 2:
type = None # Omit type context if nested function
if file is None:
file = self.file
info = ErrorInfo(self.import_context(), file, type,
self.function_or_member[-1], line, column, severity, message,
blocker, only_once)
self.add_error_info(info)
def add_error_info(self, info: ErrorInfo) -> None:
if (info.file in self.ignored_lines and
info.line in self.ignored_lines[info.file] and
not info.blocker):
# Annotation requests us to ignore all errors on this line.
self.used_ignored_lines[info.file].add(info.line)
return
if info.only_once:
if info.message in self.only_once_messages:
return
self.only_once_messages.add(info.message)
self.error_info.append(info)
def generate_unused_ignore_notes(self) -> None:
for file, ignored_lines in self.ignored_lines.items():
if not self.is_typeshed_file(file):
for line in ignored_lines - self.used_ignored_lines[file]:
# Don't use report since add_error_info will ignore the error!
info = ErrorInfo(self.import_context(), file, None, None,
line, -1, 'note', "unused 'type: ignore' comment",
False, False)
self.error_info.append(info)
def is_typeshed_file(self, file: str) -> bool:
# gross, but no other clear way to tell
return 'typeshed' in os.path.normpath(file).split(os.sep)
def num_messages(self) -> int:
"""Return the number of generated messages."""
return len(self.error_info)
def is_errors(self) -> bool:
"""Are there any generated errors?"""
return bool(self.error_info)
def is_blockers(self) -> bool:
"""Are the any errors that are blockers?"""
return any(err for err in self.error_info if err.blocker)
def raise_error(self) -> None:
"""Raise a CompileError with the generated messages.
Render the messages suitable for displaying.
"""
raise CompileError(self.messages(), use_stdout=True)
def messages(self) -> List[str]:
"""Return a string list that represents the error messages.
Use a form suitable for displaying to the user.
"""
a = [] # type: List[str]
errors = self.render_messages(self.sort_messages(self.error_info))
errors = self.remove_duplicates(errors)
for file, line, column, severity, message in errors:
s = ''
if file is not None:
if self.show_column_numbers and line is not None and line >= 0 \
and column is not None and column >= 0:
srcloc = '{}:{}:{}'.format(file, line, column)
elif line is not None and line >= 0:
srcloc = '{}:{}'.format(file, line)
else:
srcloc = file
s = '{}: {}: {}'.format(srcloc, severity, message)
else:
s = message
a.append(s)
return a
def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[str, int, int,
str, str]]:
"""Translate the messages into a sequence of tuples.
Each tuple is of form (path, line, col, message. 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 = [] # type: List[Tuple[str, int, int, str, str]]
# (path, line, column, severity, message)
prev_import_context = [] # type: List[Tuple[str, int]]
prev_function_or_member = None # type: str
prev_type = None # type: str
for e in errors:
# Report module import context, if different from previous message.
if self.hide_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, 'note', fmt.format(path, line)))
i -= 1
file = self.simplify_path(e.file)
# Report context within a source file.
if self.hide_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, 'note', 'At top level:'))
else:
result.append((file, -1, -1, 'note', 'In class "{}":'.format(
e.type)))
else:
if e.type is None:
result.append((file, -1, -1, 'note',
'In function "{}":'.format(
e.function_or_member)))
else:
result.append((file, -1, -1, 'note',
'In member "{}" of class "{}":'.format(
e.function_or_member, e.type)))
elif e.type != prev_type:
if e.type is None:
result.append((file, -1, -1, 'note', 'At top level:'))
else:
result.append((file, -1, -1, 'note',
'In class "{}":'.format(e.type)))
result.append((file, e.line, e.column, e.severity, e.message))
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 file
context by line number, but otherwise retain the general
ordering of the messages.
"""
result = [] # type: 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))
result.extend(a)
return result
def remove_duplicates(self, errors: List[Tuple[str, int, int, str, str]]
) -> List[Tuple[str, int, int, str, str]]:
"""Remove duplicates from a sorted error list."""
res = [] # type: List[Tuple[str, int, int, str, str]]
i = 0
while i < len(errors):
dup = False
j = i - 1
while (j >= 0 and errors[j][0] == errors[i][0] and
errors[j][1] == errors[i][1]):
if (errors[j][3] == errors[i][3] and
errors[j][4] == errors[i][4]): # 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.
"""
messages = None # type: List[str]
use_stdout = False
def __init__(self, messages: List[str], use_stdout: bool = False) -> None:
super().__init__('\n'.join(messages))
self.messages = messages
self.use_stdout = use_stdout
class DecodeError(Exception):
"""Exception raised when a file cannot be decoded due to an unknown encoding type.
Essentially a wrapper for the LookupError raised by `bytearray.decode`
"""
def remove_path_prefix(path: str, prefix: str) -> 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, line: int,
errors: Errors, options: Options) -> None:
"""Report internal error and exit.
This optionally starts pdb or shows a traceback.
"""
# Dump out errors so far, they often provide a clue.
# But catch unexpected errors rendering them.
try:
for msg in errors.messages():
print(msg)
except Exception as e:
print("Failed to dump errors:", repr(e), file=sys.stderr)
# Compute file:line prefix for official-looking error messages.
if line:
prefix = '{}:{}'.format(file, line)
else:
prefix = file
# Print "INTERNAL ERROR" message.
print('{}: error: INTERNAL ERROR --'.format(prefix),
'please report a bug at https://github.com/python/mypy/issues',
file=sys.stderr)
# If requested, drop into pdb. This overrides show_tb.
if options.pdb:
print('Dropping into pdb', file=sys.stderr)
import pdb
pdb.post_mortem(sys.exc_info()[2])
# If requested, print traceback, else print note explaining how to get one.
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=sys.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('{}: {}'.format(type(err).__name__, err))
print('{}: note: use --pdb to drop into pdb'.format(prefix), file=sys.stderr)
# Exit. The caller has nothing more to say.
raise SystemExit(1)