Skip to content

Print and show error end locations #13148

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

Merged
merged 8 commits into from
Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,14 @@ in error messages.

main.py:12:9: error: Unsupported operand types for / ("int" and "str")

.. option:: --show-error-end

This flag will make mypy show not just that start position where
an error was detected, but also the end position of the relevant expression.
This way various tools can easily highlight the whole error span. The format is
``file:line:column:end_line:end_column``. This option implies
``--show-column-numbers``.

.. option:: --show-error-codes

This flag will add an error code ``[<code>]`` to error messages. The error
Expand Down
1 change: 1 addition & 0 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ def _build(sources: List[BuildSource],
options.show_column_numbers,
options.show_error_codes,
options.pretty,
options.show_error_end,
lambda path: read_py_file(path, cached_read, options.python_version),
options.show_absolute_path,
options.enabled_error_codes,
Expand Down
85 changes: 60 additions & 25 deletions mypy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ class ErrorInfo:
# 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 = ''

Expand Down Expand Up @@ -87,6 +93,8 @@ def __init__(self,
function_or_member: Optional[str],
line: int,
column: int,
end_line: int,
end_column: int,
severity: str,
message: str,
code: Optional[ErrorCode],
Expand All @@ -102,6 +110,8 @@ def __init__(self,
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
Expand All @@ -113,8 +123,10 @@ def __init__(self,


# Type used internally to represent errors:
# (path, line, column, severity, message, allow_dups, code)
# (path, line, column, end_line, end_column, severity, message, allow_dups, code)
ErrorTuple = Tuple[Optional[str],
int,
int,
int,
int,
str,
Expand Down Expand Up @@ -221,6 +233,10 @@ class Errors:
# 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

Expand All @@ -241,6 +257,7 @@ def __init__(self,
show_column_numbers: bool = False,
show_error_codes: bool = False,
pretty: bool = False,
show_error_end: bool = False,
read_source: Optional[Callable[[str], Optional[List[str]]]] = None,
show_absolute_path: bool = False,
enabled_error_codes: Optional[Set[ErrorCode]] = None,
Expand All @@ -251,6 +268,9 @@ def __init__(self,
self.show_error_codes = show_error_codes
self.show_absolute_path = show_absolute_path
self.pretty = pretty
self.show_error_end = show_error_end
if show_error_end:
assert show_column_numbers, "Inconsistent formatting, must be prevented by argparse"
# We use fscache to read source code when showing snippets.
self.read_source = read_source
self.enabled_error_codes = enabled_error_codes or set()
Expand Down Expand Up @@ -343,7 +363,8 @@ def report(self,
allow_dups: bool = False,
origin_line: Optional[int] = None,
offset: int = 0,
end_line: Optional[int] = None) -> None:
end_line: Optional[int] = None,
end_column: Optional[int] = None) -> None:
"""Report message at the given line using the current error context.

Args:
Expand All @@ -370,6 +391,12 @@ def report(self,

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:
Expand All @@ -384,7 +411,7 @@ def report(self,
code = code or (codes.MISC if not blocker else None)

info = ErrorInfo(self.import_context(), file, self.current_module(), type,
function, line, column, severity, message, code,
function, line, column, end_line, end_column, severity, message, code,
blocker, only_once, allow_dups,
origin=(self.file, origin_line, end_line),
target=self.current_target())
Expand Down Expand Up @@ -470,7 +497,7 @@ def add_error_info(self, info: ErrorInfo) -> None:
'may be out of date')
note = ErrorInfo(
info.import_ctx, info.file, info.module, info.type, info.function_or_member,
info.line, info.column, 'note', msg,
info.line, info.column, info.end_line, info.end_column, 'note', msg,
code=None, blocker=False, only_once=False, allow_dups=False
)
self._add_error_info(file, note)
Expand Down Expand Up @@ -500,7 +527,9 @@ def report_hidden_errors(self, info: ErrorInfo) -> None:
typ=None,
function_or_member=None,
line=info.line,
column=info.line,
column=info.column,
end_line=info.end_line,
end_column=info.end_column,
severity='note',
message=message,
code=None,
Expand Down Expand Up @@ -571,7 +600,7 @@ def generate_unused_ignore_errors(self, file: str) -> None:
message = f'Unused "type: ignore{unused_codes_message}" comment'
# Don't use report since add_error_info will ignore the error!
info = ErrorInfo(self.import_context(), file, self.current_module(), None,
None, line, -1, 'error', message,
None, line, -1, line, -1, 'error', message,
None, False, False, False)
self._add_error_info(file, info)

Expand Down Expand Up @@ -606,7 +635,7 @@ def generate_ignore_without_code_errors(self,
message = f'"type: ignore" comment without error code{codes_hint}'
# Don't use report since add_error_info will ignore the error!
info = ErrorInfo(self.import_context(), file, self.current_module(), None,
None, line, -1, 'error', message, codes.IGNORE_WITHOUT_CODE,
None, line, -1, line, -1, 'error', message, codes.IGNORE_WITHOUT_CODE,
False, False, False)
self._add_error_info(file, info)

Expand Down Expand Up @@ -657,11 +686,13 @@ def format_messages(self, error_info: List[ErrorInfo],
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, severity, message, allow_dups, code in errors:
for file, line, column, end_line, end_column, severity, message, allow_dups, code in errors:
s = ''
if file is not None:
if self.show_column_numbers and line >= 0 and column >= 0:
srcloc = f'{file}:{line}:{1 + column}'
if self.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:
Expand All @@ -685,11 +716,15 @@ def format_messages(self, error_info: List[ErrorInfo],

# 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)
a.append(' ' * (DEFAULT_SOURCE_OFFSET + column) + '^')
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]:
Expand Down Expand Up @@ -763,7 +798,7 @@ def render_messages(self,
# 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',
result.append((None, -1, -1, -1, -1, 'note',
fmt.format(path, line), e.allow_dups, None))
i -= 1

Expand All @@ -776,32 +811,32 @@ def render_messages(self,
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:', e.allow_dups, None))
result.append((file, -1, -1, -1, -1, 'note', 'At top level:', e.allow_dups, None))
else:
result.append((file, -1, -1, 'note', 'In class "{}":'.format(
result.append((file, -1, -1, -1, -1, 'note', 'In class "{}":'.format(
e.type), e.allow_dups, None))
else:
if e.type is None:
result.append((file, -1, -1, 'note',
result.append((file, -1, -1, -1, -1, 'note',
'In function "{}":'.format(
e.function_or_member), e.allow_dups, None))
else:
result.append((file, -1, -1, 'note',
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, 'note', 'At top level:', e.allow_dups, None))
result.append((file, -1, -1, -1, -1, 'note', 'At top level:', e.allow_dups, None))
else:
result.append((file, -1, -1, 'note',
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.severity, e.message.value, e.allow_dups, e.code))
(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.severity, e.message, e.allow_dups, e.code))
(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
Expand Down Expand Up @@ -842,21 +877,21 @@ def remove_duplicates(self, errors: List[ErrorTuple]) -> List[ErrorTuple]:
conflicts_notes = False
j = i - 1
# Find duplicates, unless duplicates are allowed.
if not errors[i][5]:
if not errors[i][7]:
while j >= 0 and errors[j][0] == errors[i][0]:
if errors[j][4].strip() == 'Got:':
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][3] == errors[i][3] and
if (errors[j][5] == errors[i][5] and
# Allow duplicate notes in overload conflicts reporting.
not ((errors[i][3] == 'note' and
errors[i][4].strip() in allowed_duplicates)
or (errors[i][4].strip().startswith('def ') and
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][4] == errors[i][4]): # ignore column
errors[j][6] == errors[i][6]): # ignore column
dup = True
break
j -= 1
Expand Down
8 changes: 8 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,10 @@ def add_invertible_flag(flag: str,
add_invertible_flag('--show-column-numbers', default=False,
help="Show column numbers in error messages",
group=error_group)
add_invertible_flag('--show-error-end', default=False,
help="Show end line/end column numbers in error messages."
" This implies --show-column-numbers",
group=error_group)
add_invertible_flag('--show-error-codes', default=False,
help="Show error codes in error messages",
group=error_group)
Expand Down Expand Up @@ -1036,6 +1040,10 @@ def set_strict_flags() -> None:
if options.cache_fine_grained:
options.local_partial_types = True

# Implicitly show column numbers if error location end is shown
if options.show_error_end:
options.show_column_numbers = True

# Let logical_deps imply cache_fine_grained (otherwise the former is useless).
if options.logical_deps:
options.cache_fine_grained = True
Expand Down
4 changes: 3 additions & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ def report(self,
context.get_column() if context else -1,
msg, severity=severity, file=file, offset=offset,
origin_line=origin.get_line() if origin else None,
end_line=end_line, code=code, allow_dups=allow_dups)
end_line=end_line,
end_column=context.end_column if context else -1,
code=code, allow_dups=allow_dups)

def fail(self,
msg: str,
Expand Down
1 change: 1 addition & 0 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ def __init__(self) -> None:
# -- experimental options --
self.shadow_file: Optional[List[List[str]]] = None
self.show_column_numbers: bool = False
self.show_error_end: bool = False
self.show_error_codes = False
# Use soft word wrap and show trimmed source snippets with error location markers.
self.pretty = False
Expand Down
16 changes: 13 additions & 3 deletions mypy/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,16 +648,26 @@ def fit_in_terminal(self, messages: List[str],
# TODO: detecting source code highlights through an indent can be surprising.
# Restore original error message and error location.
error = error[DEFAULT_SOURCE_OFFSET:]
column = messages[i+1].index('^') - DEFAULT_SOURCE_OFFSET
marker_line = messages[i+1]
marker_column = marker_line.index('^')
column = marker_column - DEFAULT_SOURCE_OFFSET
if '~' not in marker_line:
marker = '^'
else:
# +1 because both ends are included
marker = marker_line[marker_column:marker_line.rindex('~')+1]

# Let source have some space also on the right side, plus 6
# to accommodate ... on each side.
max_len = width - DEFAULT_SOURCE_OFFSET - 6
source_line, offset = trim_source_line(error, max_len, column, MINIMUM_WIDTH)

new_messages[i] = ' ' * DEFAULT_SOURCE_OFFSET + source_line
# Also adjust the error marker position.
new_messages[i+1] = ' ' * (DEFAULT_SOURCE_OFFSET + column - offset) + '^'
# Also adjust the error marker position and trim error marker is needed.
new_marker_line = ' ' * (DEFAULT_SOURCE_OFFSET + column - offset) + marker
if len(new_marker_line) > len(new_messages[i]) and len(marker) > 3:
new_marker_line = new_marker_line[:len(new_messages[i]) - 3] + '...'
new_messages[i+1] = new_marker_line
return new_messages

def colorize(self, error: str) -> str:
Expand Down
18 changes: 18 additions & 0 deletions test-data/unit/check-columns.test
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,21 @@ def f(x: T) -> T:
[case testColumnReturnValueExpected]
def f() -> int:
return # E:5: Return value expected

[case testCheckEndColumnPositions]
# flags: --show-error-end
x: int = "no way"

def g() -> int: ...
def f(x: str) -> None: ...
f(g(
))
x[0]
[out]
main:2:10:2:10: error: Incompatible types in assignment (expression has type "str", variable has type "int")
main:6:3:6:3: error: Argument 1 to "f" has incompatible type "int"; expected "str"
main:8:1:8:1: error: Value of type "int" is not indexable
[out version>=3.8]
main:2:10:2:17: error: Incompatible types in assignment (expression has type "str", variable has type "int")
main:6:3:7:1: error: Argument 1 to "f" has incompatible type "int"; expected "str"
main:8:1:8:4: error: Value of type "int" is not indexable
Loading