22import os .path
33import sys
44import traceback
5+ from collections import OrderedDict
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 = {}
9499 self .only_once_messages = set ()
95100
96101 def copy (self ) -> 'Errors' :
@@ -109,13 +114,22 @@ 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 )
116120
117- def set_ignored_lines (self , ignored_lines : Set [int ]) -> None :
118- self .ignored_lines = ignored_lines
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
119133
120134 def push_function (self , name : str ) -> None :
121135 """Set the current function or member short name (it can be None)."""
@@ -170,15 +184,25 @@ def report(self, line: int, message: str, blocker: bool = False,
170184 self .add_error_info (info )
171185
172186 def add_error_info (self , info : ErrorInfo ) -> None :
173- if info .line in self .ignored_lines :
187+ if info .file in self . ignored_lines and info . line in self .ignored_lines [ info . file ] :
174188 # Annotation requests us to ignore all errors on this line.
189+ self .used_ignored_lines .setdefault (info .file , set ()).add (info .line )
175190 return
176191 if info .only_once :
177192 if info .message in self .only_once_messages :
178193 return
179194 self .only_once_messages .add (info .message )
180195 self .error_info .append (info )
181196
197+ def generate_unused_ignore_notes (self ) -> None :
198+ for file , ignored_lines in self .ignored_lines .items ():
199+ for line in ignored_lines - self .used_ignored_lines .get (file , set ()):
200+ # Don't use report since add_error_info will ignore the error!
201+ info = ErrorInfo (self .import_context (), file , None , None ,
202+ line , 'note' , "unused 'type: ignore' comment" ,
203+ False , False )
204+ self .error_info .append (info )
205+
182206 def num_messages (self ) -> int :
183207 """Return the number of generated messages."""
184208 return len (self .error_info )
@@ -254,32 +278,34 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[str, int,
254278 result .append ((None , - 1 , 'note' , fmt .format (path , line )))
255279 i -= 1
256280
281+ file = self .simplify_path (e .file )
282+
257283 # Report context within a source file.
258284 if (e .function_or_member != prev_function_or_member or
259285 e .type != prev_type ):
260286 if e .function_or_member is None :
261287 if e .type is None :
262- result .append ((e . file , - 1 , 'note' , 'At top level:' ))
288+ result .append ((file , - 1 , 'note' , 'At top level:' ))
263289 else :
264- result .append ((e . file , - 1 , 'note' , 'In class "{}":' .format (
290+ result .append ((file , - 1 , 'note' , 'In class "{}":' .format (
265291 e .type )))
266292 else :
267293 if e .type is None :
268- result .append ((e . file , - 1 , 'note' ,
294+ result .append ((file , - 1 , 'note' ,
269295 'In function "{}":' .format (
270296 e .function_or_member )))
271297 else :
272- result .append ((e . file , - 1 , 'note' ,
298+ result .append ((file , - 1 , 'note' ,
273299 'In member "{}" of class "{}":' .format (
274300 e .function_or_member , e .type )))
275301 elif e .type != prev_type :
276302 if e .type is None :
277- result .append ((e . file , - 1 , 'note' , 'At top level:' ))
303+ result .append ((file , - 1 , 'note' , 'At top level:' ))
278304 else :
279- result .append ((e . file , - 1 , 'note' ,
305+ result .append ((file , - 1 , 'note' ,
280306 'In class "{}":' .format (e .type )))
281307
282- result .append ((e . file , e .line , e .severity , e .message ))
308+ result .append ((file , e .line , e .severity , e .message ))
283309
284310 prev_import_context = e .import_ctx
285311 prev_function_or_member = e .function_or_member
0 commit comments