@@ -50,6 +50,12 @@ class ErrorInfo:
5050 # The column number related to this error with file.
5151 column = 0 # -1 if unknown
5252
53+ # The end line number related to this error within file.
54+ end_line = 0 # -1 if unknown
55+
56+ # The end column number related to this error with file.
57+ end_column = 0 # -1 if unknown
58+
5359 # Either 'error' or 'note'
5460 severity = ''
5561
@@ -87,6 +93,8 @@ def __init__(self,
8793 function_or_member : Optional [str ],
8894 line : int ,
8995 column : int ,
96+ end_line : int ,
97+ end_column : int ,
9098 severity : str ,
9199 message : str ,
92100 code : Optional [ErrorCode ],
@@ -102,6 +110,8 @@ def __init__(self,
102110 self .function_or_member = function_or_member
103111 self .line = line
104112 self .column = column
113+ self .end_line = end_line
114+ self .end_column = end_column
105115 self .severity = severity
106116 self .message = message
107117 self .code = code
@@ -113,8 +123,10 @@ def __init__(self,
113123
114124
115125# Type used internally to represent errors:
116- # (path, line, column, severity, message, allow_dups, code)
126+ # (path, line, column, end_line, end_column, severity, message, allow_dups, code)
117127ErrorTuple = Tuple [Optional [str ],
128+ int ,
129+ int ,
118130 int ,
119131 int ,
120132 str ,
@@ -221,6 +233,10 @@ class Errors:
221233 # Set to True to show column numbers in error messages.
222234 show_column_numbers : bool = False
223235
236+ # Set to True to show end line and end column in error messages.
237+ # Ths implies `show_column_numbers`.
238+ show_error_end : bool = False
239+
224240 # Set to True to show absolute file paths in error messages.
225241 show_absolute_path : bool = False
226242
@@ -241,6 +257,7 @@ def __init__(self,
241257 show_column_numbers : bool = False ,
242258 show_error_codes : bool = False ,
243259 pretty : bool = False ,
260+ show_error_end : bool = False ,
244261 read_source : Optional [Callable [[str ], Optional [List [str ]]]] = None ,
245262 show_absolute_path : bool = False ,
246263 enabled_error_codes : Optional [Set [ErrorCode ]] = None ,
@@ -251,6 +268,9 @@ def __init__(self,
251268 self .show_error_codes = show_error_codes
252269 self .show_absolute_path = show_absolute_path
253270 self .pretty = pretty
271+ self .show_error_end = show_error_end
272+ if show_error_end :
273+ assert show_column_numbers , "Inconsistent formatting, must be prevented by argparse"
254274 # We use fscache to read source code when showing snippets.
255275 self .read_source = read_source
256276 self .enabled_error_codes = enabled_error_codes or set ()
@@ -343,7 +363,8 @@ def report(self,
343363 allow_dups : bool = False ,
344364 origin_line : Optional [int ] = None ,
345365 offset : int = 0 ,
346- end_line : Optional [int ] = None ) -> None :
366+ end_line : Optional [int ] = None ,
367+ end_column : Optional [int ] = None ) -> None :
347368 """Report message at the given line using the current error context.
348369
349370 Args:
@@ -370,6 +391,12 @@ def report(self,
370391
371392 if column is None :
372393 column = - 1
394+ if end_column is None :
395+ if column == - 1 :
396+ end_column = - 1
397+ else :
398+ end_column = column + 1
399+
373400 if file is None :
374401 file = self .file
375402 if offset :
@@ -384,7 +411,7 @@ def report(self,
384411 code = code or (codes .MISC if not blocker else None )
385412
386413 info = ErrorInfo (self .import_context (), file , self .current_module (), type ,
387- function , line , column , severity , message , code ,
414+ function , line , column , end_line , end_column , severity , message , code ,
388415 blocker , only_once , allow_dups ,
389416 origin = (self .file , origin_line , end_line ),
390417 target = self .current_target ())
@@ -470,7 +497,7 @@ def add_error_info(self, info: ErrorInfo) -> None:
470497 'may be out of date' )
471498 note = ErrorInfo (
472499 info .import_ctx , info .file , info .module , info .type , info .function_or_member ,
473- info .line , info .column , 'note' , msg ,
500+ info .line , info .column , info . end_line , info . end_column , 'note' , msg ,
474501 code = None , blocker = False , only_once = False , allow_dups = False
475502 )
476503 self ._add_error_info (file , note )
@@ -500,7 +527,9 @@ def report_hidden_errors(self, info: ErrorInfo) -> None:
500527 typ = None ,
501528 function_or_member = None ,
502529 line = info .line ,
503- column = info .line ,
530+ column = info .column ,
531+ end_line = info .end_line ,
532+ end_column = info .end_column ,
504533 severity = 'note' ,
505534 message = message ,
506535 code = None ,
@@ -571,7 +600,7 @@ def generate_unused_ignore_errors(self, file: str) -> None:
571600 message = f'Unused "type: ignore{ unused_codes_message } " comment'
572601 # Don't use report since add_error_info will ignore the error!
573602 info = ErrorInfo (self .import_context (), file , self .current_module (), None ,
574- None , line , - 1 , 'error' , message ,
603+ None , line , - 1 , line , - 1 , 'error' , message ,
575604 None , False , False , False )
576605 self ._add_error_info (file , info )
577606
@@ -606,7 +635,7 @@ def generate_ignore_without_code_errors(self,
606635 message = f'"type: ignore" comment without error code{ codes_hint } '
607636 # Don't use report since add_error_info will ignore the error!
608637 info = ErrorInfo (self .import_context (), file , self .current_module (), None ,
609- None , line , - 1 , 'error' , message , codes .IGNORE_WITHOUT_CODE ,
638+ None , line , - 1 , line , - 1 , 'error' , message , codes .IGNORE_WITHOUT_CODE ,
610639 False , False , False )
611640 self ._add_error_info (file , info )
612641
@@ -657,11 +686,13 @@ def format_messages(self, error_info: List[ErrorInfo],
657686 error_info = [info for info in error_info if not info .hidden ]
658687 errors = self .render_messages (self .sort_messages (error_info ))
659688 errors = self .remove_duplicates (errors )
660- for file , line , column , severity , message , allow_dups , code in errors :
689+ for file , line , column , end_line , end_column , severity , message , allow_dups , code in errors :
661690 s = ''
662691 if file is not None :
663692 if self .show_column_numbers and line >= 0 and column >= 0 :
664693 srcloc = f'{ file } :{ line } :{ 1 + column } '
694+ if self .show_error_end and end_line >= 0 and end_column >= 0 :
695+ srcloc += f':{ end_line } :{ end_column } '
665696 elif line >= 0 :
666697 srcloc = f'{ file } :{ line } '
667698 else :
@@ -685,11 +716,15 @@ def format_messages(self, error_info: List[ErrorInfo],
685716
686717 # Shifts column after tab expansion
687718 column = len (source_line [:column ].expandtabs ())
719+ end_column = len (source_line [:end_column ].expandtabs ())
688720
689721 # Note, currently coloring uses the offset to detect source snippets,
690722 # so these offsets should not be arbitrary.
691723 a .append (' ' * DEFAULT_SOURCE_OFFSET + source_line_expanded )
692- a .append (' ' * (DEFAULT_SOURCE_OFFSET + column ) + '^' )
724+ marker = '^'
725+ if end_line == line and end_column > column :
726+ marker = f'^{ "~" * (end_column - column - 1 )} '
727+ a .append (' ' * (DEFAULT_SOURCE_OFFSET + column ) + marker )
693728 return a
694729
695730 def file_messages (self , path : str ) -> List [str ]:
@@ -763,7 +798,7 @@ def render_messages(self,
763798 # Remove prefix to ignore from path (if present) to
764799 # simplify path.
765800 path = remove_path_prefix (path , self .ignore_prefix )
766- result .append ((None , - 1 , - 1 , 'note' ,
801+ result .append ((None , - 1 , - 1 , - 1 , - 1 , 'note' ,
767802 fmt .format (path , line ), e .allow_dups , None ))
768803 i -= 1
769804
@@ -776,32 +811,32 @@ def render_messages(self,
776811 e .type != prev_type ):
777812 if e .function_or_member is None :
778813 if e .type is None :
779- result .append ((file , - 1 , - 1 , 'note' , 'At top level:' , e .allow_dups , None ))
814+ result .append ((file , - 1 , - 1 , - 1 , - 1 , 'note' , 'At top level:' , e .allow_dups , None ))
780815 else :
781- result .append ((file , - 1 , - 1 , 'note' , 'In class "{}":' .format (
816+ result .append ((file , - 1 , - 1 , - 1 , - 1 , 'note' , 'In class "{}":' .format (
782817 e .type ), e .allow_dups , None ))
783818 else :
784819 if e .type is None :
785- result .append ((file , - 1 , - 1 , 'note' ,
820+ result .append ((file , - 1 , - 1 , - 1 , - 1 , 'note' ,
786821 'In function "{}":' .format (
787822 e .function_or_member ), e .allow_dups , None ))
788823 else :
789- result .append ((file , - 1 , - 1 , 'note' ,
824+ result .append ((file , - 1 , - 1 , - 1 , - 1 , 'note' ,
790825 'In member "{}" of class "{}":' .format (
791826 e .function_or_member , e .type ), e .allow_dups , None ))
792827 elif e .type != prev_type :
793828 if e .type is None :
794- result .append ((file , - 1 , - 1 , 'note' , 'At top level:' , e .allow_dups , None ))
829+ result .append ((file , - 1 , - 1 , - 1 , - 1 , 'note' , 'At top level:' , e .allow_dups , None ))
795830 else :
796- result .append ((file , - 1 , - 1 , 'note' ,
831+ result .append ((file , - 1 , - 1 , - 1 , - 1 , 'note' ,
797832 f'In class "{ e .type } ":' , e .allow_dups , None ))
798833
799834 if isinstance (e .message , ErrorMessage ):
800835 result .append (
801- (file , e .line , e .column , e .severity , e .message .value , e .allow_dups , e .code ))
836+ (file , e .line , e .column , e .end_line , e . end_column , e . severity , e .message .value , e .allow_dups , e .code ))
802837 else :
803838 result .append (
804- (file , e .line , e .column , e .severity , e .message , e .allow_dups , e .code ))
839+ (file , e .line , e .column , e .end_line , e . end_column , e . severity , e .message , e .allow_dups , e .code ))
805840
806841 prev_import_context = e .import_ctx
807842 prev_function_or_member = e .function_or_member
@@ -842,21 +877,21 @@ def remove_duplicates(self, errors: List[ErrorTuple]) -> List[ErrorTuple]:
842877 conflicts_notes = False
843878 j = i - 1
844879 # Find duplicates, unless duplicates are allowed.
845- if not errors [i ][5 ]:
880+ if not errors [i ][7 ]:
846881 while j >= 0 and errors [j ][0 ] == errors [i ][0 ]:
847- if errors [j ][4 ].strip () == 'Got:' :
882+ if errors [j ][6 ].strip () == 'Got:' :
848883 conflicts_notes = True
849884 j -= 1
850885 j = i - 1
851886 while (j >= 0 and errors [j ][0 ] == errors [i ][0 ] and
852887 errors [j ][1 ] == errors [i ][1 ]):
853- if (errors [j ][3 ] == errors [i ][3 ] and
888+ if (errors [j ][5 ] == errors [i ][5 ] and
854889 # Allow duplicate notes in overload conflicts reporting.
855- not ((errors [i ][3 ] == 'note' and
856- errors [i ][4 ].strip () in allowed_duplicates )
857- or (errors [i ][4 ].strip ().startswith ('def ' ) and
890+ not ((errors [i ][5 ] == 'note' and
891+ errors [i ][6 ].strip () in allowed_duplicates )
892+ or (errors [i ][6 ].strip ().startswith ('def ' ) and
858893 conflicts_notes )) and
859- errors [j ][4 ] == errors [i ][4 ]): # ignore column
894+ errors [j ][6 ] == errors [i ][6 ]): # ignore column
860895 dup = True
861896 break
862897 j -= 1
0 commit comments