22import os .path
33import sys
44import traceback
5+ from collections import OrderedDict , defaultdict
56
6- from typing import Tuple , List , TypeVar , Set
7+ from typing import Tuple , List , TypeVar , Set , Dict , Optional
78
89
910T = TypeVar ('T' )
@@ -79,8 +80,11 @@ class Errors:
7980 # Stack of short names of current functions or members (or None).
8081 function_or_member = None # type: List[str]
8182
82- # Ignore errors on these lines.
83- ignored_lines = None # type: Set[int]
83+ # Ignore errors on these lines of each file.
84+ ignored_lines = None # type: Dict[str, Set[int]]
85+
86+ # Lines on which an error was actually ignored.
87+ used_ignored_lines = None # type: Dict[str, Set[int]]
8488
8589 # Collection of reported only_once messages.
8690 only_once_messages = None # type: Set[str]
@@ -90,7 +94,8 @@ def __init__(self) -> None:
9094 self .import_ctx = []
9195 self .type_name = [None ]
9296 self .function_or_member = [None ]
93- self .ignored_lines = set ()
97+ self .ignored_lines = OrderedDict ()
98+ self .used_ignored_lines = defaultdict (set )
9499 self .only_once_messages = set ()
95100
96101 def copy (self ) -> 'Errors' :
@@ -109,13 +114,26 @@ def set_ignore_prefix(self, prefix: str) -> None:
109114 prefix += os .sep
110115 self .ignore_prefix = prefix
111116
112- def set_file (self , file : str ) -> None :
113- """Set the path of the current file."""
117+ def simplify_path (self , file : str ) -> str :
114118 file = os .path .normpath (file )
115- self .file = remove_path_prefix (file , self .ignore_prefix )
119+ return remove_path_prefix (file , self .ignore_prefix )
120+
121+ def set_file (self , file : str , ignored_lines : Set [int ] = None ) -> None :
122+ """Set the path of the current file."""
123+ # The path will be simplified later, in render_messages. That way
124+ # * 'file' is always a key that uniquely identifies a source file
125+ # that mypy read (simplified paths might not be unique); and
126+ # * we only have to simplify in one place, while still supporting
127+ # reporting errors for files other than the one currently being
128+ # processed.
129+ self .file = file
130+
131+ def set_file_ignored_lines (self , file : str , ignored_lines : Set [int ] = None ) -> None :
132+ self .ignored_lines [file ] = ignored_lines
116133
117- def set_ignored_lines (self , ignored_lines : Set [int ]) -> None :
118- self .ignored_lines = ignored_lines
134+ def mark_file_ignored_lines_used (self , file : str , used_ignored_lines : Set [int ] = None
135+ ) -> None :
136+ self .used_ignored_lines [file ] |= used_ignored_lines
119137
120138 def push_function (self , name : str ) -> None :
121139 """Set the current function or member short name (it can be None)."""
@@ -170,15 +188,25 @@ def report(self, line: int, message: str, blocker: bool = False,
170188 self .add_error_info (info )
171189
172190 def add_error_info (self , info : ErrorInfo ) -> None :
173- if info .line in self .ignored_lines :
191+ if info .file in self . ignored_lines and info . line in self .ignored_lines [ info . file ] :
174192 # Annotation requests us to ignore all errors on this line.
193+ self .used_ignored_lines [info .file ].add (info .line )
175194 return
176195 if info .only_once :
177196 if info .message in self .only_once_messages :
178197 return
179198 self .only_once_messages .add (info .message )
180199 self .error_info .append (info )
181200
201+ def generate_unused_ignore_notes (self ) -> None :
202+ for file , ignored_lines in self .ignored_lines .items ():
203+ for line in ignored_lines - self .used_ignored_lines [file ]:
204+ # Don't use report since add_error_info will ignore the error!
205+ info = ErrorInfo (self .import_context (), file , None , None ,
206+ line , 'note' , "unused 'type: ignore' comment" ,
207+ False , False )
208+ self .error_info .append (info )
209+
182210 def num_messages (self ) -> int :
183211 """Return the number of generated messages."""
184212 return len (self .error_info )
@@ -254,32 +282,34 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[str, int,
254282 result .append ((None , - 1 , 'note' , fmt .format (path , line )))
255283 i -= 1
256284
285+ file = self .simplify_path (e .file )
286+
257287 # Report context within a source file.
258288 if (e .function_or_member != prev_function_or_member or
259289 e .type != prev_type ):
260290 if e .function_or_member is None :
261291 if e .type is None :
262- result .append ((e . file , - 1 , 'note' , 'At top level:' ))
292+ result .append ((file , - 1 , 'note' , 'At top level:' ))
263293 else :
264- result .append ((e . file , - 1 , 'note' , 'In class "{}":' .format (
294+ result .append ((file , - 1 , 'note' , 'In class "{}":' .format (
265295 e .type )))
266296 else :
267297 if e .type is None :
268- result .append ((e . file , - 1 , 'note' ,
298+ result .append ((file , - 1 , 'note' ,
269299 'In function "{}":' .format (
270300 e .function_or_member )))
271301 else :
272- result .append ((e . file , - 1 , 'note' ,
302+ result .append ((file , - 1 , 'note' ,
273303 'In member "{}" of class "{}":' .format (
274304 e .function_or_member , e .type )))
275305 elif e .type != prev_type :
276306 if e .type is None :
277- result .append ((e . file , - 1 , 'note' , 'At top level:' ))
307+ result .append ((file , - 1 , 'note' , 'At top level:' ))
278308 else :
279- result .append ((e . file , - 1 , 'note' ,
309+ result .append ((file , - 1 , 'note' ,
280310 'In class "{}":' .format (e .type )))
281311
282- result .append ((e . file , e .line , e .severity , e .message ))
312+ result .append ((file , e .line , e .severity , e .message ))
283313
284314 prev_import_context = e .import_ctx
285315 prev_function_or_member = e .function_or_member
0 commit comments