From 987edf98c5d0ed5d94f8edda71d2723c26c24799 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 16 Jul 2022 14:27:33 +0100 Subject: [PATCH 1/8] Support end position reporting for errors --- mypy/build.py | 1 + mypy/errors.py | 79 +++++++++++++++++++++---------- mypy/main.py | 8 ++++ mypy/messages.py | 4 +- mypy/options.py | 1 + mypy/util.py | 9 +++- test-data/unit/check-columns.test | 5 ++ test-data/unit/cmdline.test | 6 +-- 8 files changed, 83 insertions(+), 30 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 1196356d5bb8..1829ad4d0694 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -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, diff --git a/mypy/errors.py b/mypy/errors.py index 0ad56b079ecc..ab1842a9c36d 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -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 = '' @@ -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], @@ -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 @@ -115,6 +125,8 @@ def __init__(self, # Type used internally to represent errors: # (path, line, column, severity, message, allow_dups, code) ErrorTuple = Tuple[Optional[str], + int, + int, int, int, str, @@ -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 @@ -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, @@ -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() @@ -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: @@ -370,6 +391,9 @@ def report(self, if column is None: column = -1 + if end_column is None: + end_column = column + if file is None: file = self.file if offset: @@ -384,7 +408,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()) @@ -470,7 +494,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) @@ -500,7 +524,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, @@ -571,7 +597,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) @@ -606,7 +632,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) @@ -657,11 +683,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: @@ -689,7 +717,10 @@ def format_messages(self, error_info: List[ErrorInfo], # 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]: @@ -763,7 +794,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 @@ -776,32 +807,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 @@ -842,21 +873,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 diff --git a/mypy/main.py b/mypy/main.py index 14b318ead3e7..a36bb30b293f 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -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) @@ -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 diff --git a/mypy/messages.py b/mypy/messages.py index 628c2cbaf0a4..47d8669a6c4f 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -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, diff --git a/mypy/options.py b/mypy/options.py index 254af61a0645..de76d549b20f 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -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 diff --git a/mypy/util.py b/mypy/util.py index 03db281ef615..ea7840b42b73 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -648,7 +648,9 @@ 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_column = messages[i+1].index('^') + column = marker_column - DEFAULT_SOURCE_OFFSET + marker = messages[i+1][marker_column:messages[i+1].rindex('~')+1] # Let source have some space also on the right side, plus 6 # to accommodate ... on each side. @@ -657,7 +659,10 @@ def fit_in_terminal(self, messages: List[str], new_messages[i] = ' ' * DEFAULT_SOURCE_OFFSET + source_line # Also adjust the error marker position. - new_messages[i+1] = ' ' * (DEFAULT_SOURCE_OFFSET + column - offset) + '^' + new_marker_line = ' ' * (DEFAULT_SOURCE_OFFSET + column - offset) + marker + if len(new_marker_line) > len(new_messages[i]): + 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: diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index 3220119748e3..9f2656687895 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -421,3 +421,8 @@ def f(x: T) -> T: [case testColumnReturnValueExpected] def f() -> int: return # E:5: Return value expected + +[case testEndPositions] +# flags: --show-error-end +x: int = "no way" +[out] diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index 3b3b64c038c1..ce21105fb49f 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -1128,16 +1128,16 @@ class AnotherCustomClassDefinedBelow: [out] some_file.py:3: error: Unsupported operand types for + ("int" and "str") 42 + 'no way' - ^ + ^~~~~~~~ some_file.py:11: error: Incompatible types in assignment (expression has type "AnotherCustomClassDefinedBelow", variable has type "OneCustomClassName") ...t_attribute_with_long_name: OneCustomClassName = OneCustomClassName().... - ^ + ^~~~~~~~~~~~~~~~~~~~~... some_file.py:11: error: Argument 1 to "some_interesting_method" of "OneCustomClassName" has incompatible type "Union[int, str, float]"; expected "AnotherCustomClassDefinedBelow" ...OneCustomClassName = OneCustomClassName().some_interesting_method(arg) - ^ + ^~~ [case testShowSourceCodeSnippetsBlockingError] # cmd: mypy --pretty --show-error-codes some_file.py From c813bd2ef88041ac60bca8c7c710fdde599f006a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 16 Jul 2022 14:37:07 +0100 Subject: [PATCH 2/8] Update test --- test-data/unit/check-columns.test | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index 9f2656687895..e56dc55faf82 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -422,7 +422,16 @@ def f(x: T) -> T: def f() -> int: return # E:5: Return value expected -[case testEndPositions] +[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: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 \ No newline at end of file From 0e5bb0c351c4babaeb6420f86da560796517dadb Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 16 Jul 2022 15:58:41 +0100 Subject: [PATCH 3/8] Some fixes --- docs/source/command_line.rst | 8 ++++++++ mypy/util.py | 11 ++++++++--- test-data/unit/check-columns.test | 2 +- test-data/unit/cmdline.test | 2 +- test-data/unit/fine-grained-blockers.test | 4 ++-- test-data/unit/fine-grained.test | 4 ++-- 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 10416766f261..7955d755e797 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -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 ``[]`` to error messages. The error diff --git a/mypy/util.py b/mypy/util.py index ea7840b42b73..29aff719a6f0 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -648,9 +648,14 @@ 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:] - marker_column = messages[i+1].index('^') + marker_line = messages[i+1] + marker_column = marker_line.index('^') column = marker_column - DEFAULT_SOURCE_OFFSET - marker = messages[i+1][marker_column:messages[i+1].rindex('~')+1] + 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. @@ -658,7 +663,7 @@ def fit_in_terminal(self, messages: List[str], 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. + # 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]): new_marker_line = new_marker_line[:len(new_messages[i]) - 3] + '...' diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index e56dc55faf82..d9b2b6db25ba 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -434,4 +434,4 @@ x[0] [out] 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 \ No newline at end of file +main:8:1:8:4: error: Value of type "int" is not indexable diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index ce21105fb49f..23e76e924868 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -1112,7 +1112,7 @@ mypy: can't read file 'missing.py': No such file or directory == Return code: 2 [case testShowSourceCodeSnippetsWrappedFormatting] -# cmd: mypy --pretty --python-version=3.6 some_file.py +# cmd: mypy --pretty --python-version=3.8 some_file.py [file some_file.py] from typing import Union diff --git a/test-data/unit/fine-grained-blockers.test b/test-data/unit/fine-grained-blockers.test index 66a68115afa5..d82a3bb5946f 100644 --- a/test-data/unit/fine-grained-blockers.test +++ b/test-data/unit/fine-grained-blockers.test @@ -31,7 +31,7 @@ main:2: error: Missing positional argument "x" in call to "f" == [case testParseErrorShowSource] -# flags: --pretty --show-error-codes +# flags: --pretty --show-error-codes --python-version=3.8 import a a.f() [file a.py] @@ -50,7 +50,7 @@ a.py:1: error: invalid syntax [syntax] == main:3: error: Missing positional argument "x" in call to "f" [call-arg] a.f() - ^ + ^~~~~ == [out version>=3.10] == diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index c2bd67320f3f..0ba920035a81 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -82,7 +82,7 @@ class A: main:4: error: Missing positional argument "a" in call to "g" of "A" [case testReprocessMethodShowSource] -# flags: --pretty --show-error-codes +# flags: --pretty --show-error-codes --python-version=3.8 import m class B: def f(self, a: m.A) -> None: @@ -97,7 +97,7 @@ class A: == main:5: error: Missing positional argument "a" in call to "g" of "A" [call-arg] a.g() # E - ^ + ^~~~~ [case testFunctionMissingModuleAttribute] import m From 096d683931f1366c386c18ee1f4ca730aead7fc1 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 16 Jul 2022 17:10:16 +0100 Subject: [PATCH 4/8] Fix Python versions --- test-data/unit/cmdline.test | 14 +++++++++++++- test-data/unit/fine-grained-blockers.test | 12 +++++++++++- test-data/unit/fine-grained.test | 5 +++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index 23e76e924868..9c25ebf3ba23 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -1126,6 +1126,19 @@ class AnotherCustomClassDefinedBelow: def another_even_more_interesting_method(self, arg: Union[int, str, float]) -> None: self.very_important_attribute_with_long_name: OneCustomClassName = OneCustomClassName().some_interesting_method(arg) [out] +some_file.py:3: error: Unsupported operand types for + ("int" and "str") + 42 + 'no way' + ^ +some_file.py:11: error: Incompatible types in assignment (expression has type +"AnotherCustomClassDefinedBelow", variable has type "OneCustomClassName") + ...t_attribute_with_long_name: OneCustomClassName = OneCustomClassName().... + ^ +some_file.py:11: error: Argument 1 to "some_interesting_method" of +"OneCustomClassName" has incompatible type "Union[int, str, float]"; expected +"AnotherCustomClassDefinedBelow" + ...OneCustomClassName = OneCustomClassName().some_interesting_method(arg) + ^ +[out version>=3.8] some_file.py:3: error: Unsupported operand types for + ("int" and "str") 42 + 'no way' ^~~~~~~~ @@ -1138,7 +1151,6 @@ some_file.py:11: error: Argument 1 to "some_interesting_method" of "AnotherCustomClassDefinedBelow" ...OneCustomClassName = OneCustomClassName().some_interesting_method(arg) ^~~ - [case testShowSourceCodeSnippetsBlockingError] # cmd: mypy --pretty --show-error-codes some_file.py [file some_file.py] diff --git a/test-data/unit/fine-grained-blockers.test b/test-data/unit/fine-grained-blockers.test index d82a3bb5946f..1d0018f27337 100644 --- a/test-data/unit/fine-grained-blockers.test +++ b/test-data/unit/fine-grained-blockers.test @@ -48,6 +48,16 @@ a.py:1: error: invalid syntax [syntax] def f(x: int) -> ^ == +main:3: error: Missing positional argument "x" in call to "f" [call-arg] + a.f() + ^ +== +[out version>=3.8] +== +a.py:1: error: invalid syntax [syntax] + def f(x: int) -> + ^ +== main:3: error: Missing positional argument "x" in call to "f" [call-arg] a.f() ^~~~~ @@ -60,7 +70,7 @@ a.py:1: error: expected ':' [syntax] == main:3: error: Missing positional argument "x" in call to "f" [call-arg] a.f() - ^ + ^~~~~ == [case testParseErrorMultipleTimes] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 0ba920035a81..25ea2b3c7e4a 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -95,6 +95,11 @@ class A: def g(self, a: A) -> None: pass [out] == +main:5: error: Missing positional argument "a" in call to "g" of "A" [call-arg] + a.g() # E + ^ +[out version >= 3.8] +== main:5: error: Missing positional argument "a" in call to "g" of "A" [call-arg] a.g() # E ^~~~~ From 79134558d279405d932793508dadffbe652524d6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 16 Jul 2022 17:20:10 +0100 Subject: [PATCH 5/8] Fix another test --- mypy/errors.py | 2 +- test-data/unit/check-columns.test | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/errors.py b/mypy/errors.py index ab1842a9c36d..e8ff5a6ac56d 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -392,7 +392,7 @@ def report(self, if column is None: column = -1 if end_column is None: - end_column = column + end_column = column + 1 if file is None: file = self.file diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index d9b2b6db25ba..73bddec130c7 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -432,6 +432,10 @@ 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 From 65832357efa1b4948c7a767ca63de34a307ac878 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 16 Jul 2022 17:32:55 +0100 Subject: [PATCH 6/8] Fix tests for real --- test-data/unit/check-columns.test | 2 +- test-data/unit/cmdline.test | 2 +- test-data/unit/fine-grained-blockers.test | 2 +- test-data/unit/fine-grained.test | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index 73bddec130c7..219befe1bb7d 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -435,7 +435,7 @@ x[0] 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] +[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 diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index 9c25ebf3ba23..a05d414eac42 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -1112,7 +1112,7 @@ mypy: can't read file 'missing.py': No such file or directory == Return code: 2 [case testShowSourceCodeSnippetsWrappedFormatting] -# cmd: mypy --pretty --python-version=3.8 some_file.py +# cmd: mypy --pretty some_file.py [file some_file.py] from typing import Union diff --git a/test-data/unit/fine-grained-blockers.test b/test-data/unit/fine-grained-blockers.test index 1d0018f27337..45de0013616d 100644 --- a/test-data/unit/fine-grained-blockers.test +++ b/test-data/unit/fine-grained-blockers.test @@ -31,7 +31,7 @@ main:2: error: Missing positional argument "x" in call to "f" == [case testParseErrorShowSource] -# flags: --pretty --show-error-codes --python-version=3.8 +# flags: --pretty --show-error-codes import a a.f() [file a.py] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 25ea2b3c7e4a..f58cad116bb6 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -82,7 +82,7 @@ class A: main:4: error: Missing positional argument "a" in call to "g" of "A" [case testReprocessMethodShowSource] -# flags: --pretty --show-error-codes --python-version=3.8 +# flags: --pretty --show-error-codes import m class B: def f(self, a: m.A) -> None: @@ -98,7 +98,7 @@ class A: main:5: error: Missing positional argument "a" in call to "g" of "A" [call-arg] a.g() # E ^ -[out version >= 3.8] +[out version>=3.8] == main:5: error: Missing positional argument "a" in call to "g" of "A" [call-arg] a.g() # E From 1800782624fb7a3b8f4ebb08d58add8ede3b6a33 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 17 Jul 2022 14:41:00 +0100 Subject: [PATCH 7/8] Some fixes --- mypy/errors.py | 8 ++++++-- test-data/unit/cmdline.test | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index e8ff5a6ac56d..697e87a209a3 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -123,7 +123,7 @@ 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, @@ -392,7 +392,10 @@ def report(self, if column is None: column = -1 if end_column is None: - end_column = column + 1 + if column == -1: + end_column = -1 + else: + end_column = column + 1 if file is None: file = self.file @@ -713,6 +716,7 @@ 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. diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index a05d414eac42..8f9eb0eec63c 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -1166,10 +1166,24 @@ some_file.py:1: error: invalid syntax [syntax] [file tabs.py] def test_tabs() -> str: return None +def test_between(x: str) -> None: ... +test_between(1 + 1) [out] tabs.py:2: error: Incompatible return value type (got "None", expected "str") return None ^ +tabs.py:4: error: Argument 1 to "test_between" has incompatible type "int"; +expected "str" + test_between(1 + 1) + ^ +[out version>=3.8] +tabs.py:2: error: Incompatible return value type (got "None", expected "str") + return None + ^~~~ +tabs.py:4: error: Argument 1 to "test_between" has incompatible type "int"; +expected "str" + test_between(1 + 1) + ^~~~~~~~~~~~~~ [case testErrorMessageWhenOpenPydFile] # cmd: mypy a.pyd From cca8395ea60db29551e8de7ec007b844469d3c1a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 17 Jul 2022 15:08:29 +0100 Subject: [PATCH 8/8] More fixes --- mypy/util.py | 2 +- test-data/unit/cmdline.test | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/util.py b/mypy/util.py index 29aff719a6f0..957fabbb0686 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -665,7 +665,7 @@ def fit_in_terminal(self, messages: List[str], new_messages[i] = ' ' * DEFAULT_SOURCE_OFFSET + source_line # 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]): + 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 diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index 8f9eb0eec63c..ad7a0215c1e7 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -1182,8 +1182,8 @@ tabs.py:2: error: Incompatible return value type (got "None", expected "str") ^~~~ tabs.py:4: error: Argument 1 to "test_between" has incompatible type "int"; expected "str" - test_between(1 + 1) - ^~~~~~~~~~~~~~ + test_between(1 + 1) + ^~~~~~~~~~~~ [case testErrorMessageWhenOpenPydFile] # cmd: mypy a.pyd