22import os .path
33import sys
44import traceback
5+ from collections import defaultdict , 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 = defaultdict (set )
9499 self .only_once_messages = set ()
95100
96101 def copy (self ) -> 'Errors' :
@@ -109,13 +114,14 @@ 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 :
117+ def set_file (self , file : str , ignored_lines : Set [ int ] = None ) -> None :
113118 """Set the path of the current file."""
114119 file = os .path .normpath (file )
115120 self .file = remove_path_prefix (file , self .ignore_prefix )
116-
117- def set_ignored_lines (self , ignored_lines : Set [int ]) -> None :
118- self .ignored_lines = ignored_lines
121+ if ignored_lines is not None :
122+ if self .file in self .ignored_lines :
123+ assert self .ignored_lines [self .file ] == ignored_lines
124+ self .ignored_lines [self .file ] = ignored_lines
119125
120126 def push_function (self , name : str ) -> None :
121127 """Set the current function or member short name (it can be None)."""
@@ -170,15 +176,26 @@ def report(self, line: int, message: str, blocker: bool = False,
170176 self .add_error_info (info )
171177
172178 def add_error_info (self , info : ErrorInfo ) -> None :
173- if info .line in self .ignored_lines :
179+ assert self .file == info .file
180+ if self .file in self .ignored_lines and info .line in self .ignored_lines [self .file ]:
174181 # Annotation requests us to ignore all errors on this line.
182+ self .used_ignored_lines [self .file ].add (info .line )
175183 return
176184 if info .only_once :
177185 if info .message in self .only_once_messages :
178186 return
179187 self .only_once_messages .add (info .message )
180188 self .error_info .append (info )
181189
190+ def generate_unused_ignore_notes (self ) -> None :
191+ for file , ignored_lines in self .ignored_lines .items ():
192+ for line in ignored_lines - self .used_ignored_lines [file ]:
193+ # Don't use report since add_error_info will ignore the error!
194+ info = ErrorInfo (self .import_context (), file , None , None ,
195+ line , 'note' , "unused 'type: ignore' comment" ,
196+ False , False )
197+ self .error_info .append (info )
198+
182199 def num_messages (self ) -> int :
183200 """Return the number of generated messages."""
184201 return len (self .error_info )
0 commit comments