2
2
import os .path
3
3
import sys
4
4
import traceback
5
+ from collections import OrderedDict , defaultdict
5
6
6
- from typing import Tuple , List , TypeVar , Set
7
+ from typing import Tuple , List , TypeVar , Set , Dict , Optional
7
8
8
9
9
10
T = TypeVar ('T' )
@@ -79,8 +80,11 @@ class Errors:
79
80
# Stack of short names of current functions or members (or None).
80
81
function_or_member = None # type: List[str]
81
82
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]]
84
88
85
89
# Collection of reported only_once messages.
86
90
only_once_messages = None # type: Set[str]
@@ -90,7 +94,8 @@ def __init__(self) -> None:
90
94
self .import_ctx = []
91
95
self .type_name = [None ]
92
96
self .function_or_member = [None ]
93
- self .ignored_lines = set ()
97
+ self .ignored_lines = OrderedDict ()
98
+ self .used_ignored_lines = defaultdict (set )
94
99
self .only_once_messages = set ()
95
100
96
101
def copy (self ) -> 'Errors' :
@@ -109,13 +114,26 @@ def set_ignore_prefix(self, prefix: str) -> None:
109
114
prefix += os .sep
110
115
self .ignore_prefix = prefix
111
116
112
- def set_file (self , file : str ) -> None :
113
- """Set the path of the current file."""
117
+ def simplify_path (self , file : str ) -> str :
114
118
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
116
133
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
119
137
120
138
def push_function (self , name : str ) -> None :
121
139
"""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,
170
188
self .add_error_info (info )
171
189
172
190
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 ] :
174
192
# Annotation requests us to ignore all errors on this line.
193
+ self .used_ignored_lines [info .file ].add (info .line )
175
194
return
176
195
if info .only_once :
177
196
if info .message in self .only_once_messages :
178
197
return
179
198
self .only_once_messages .add (info .message )
180
199
self .error_info .append (info )
181
200
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
+
182
210
def num_messages (self ) -> int :
183
211
"""Return the number of generated messages."""
184
212
return len (self .error_info )
@@ -254,32 +282,34 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[str, int,
254
282
result .append ((None , - 1 , 'note' , fmt .format (path , line )))
255
283
i -= 1
256
284
285
+ file = self .simplify_path (e .file )
286
+
257
287
# Report context within a source file.
258
288
if (e .function_or_member != prev_function_or_member or
259
289
e .type != prev_type ):
260
290
if e .function_or_member is None :
261
291
if e .type is None :
262
- result .append ((e . file , - 1 , 'note' , 'At top level:' ))
292
+ result .append ((file , - 1 , 'note' , 'At top level:' ))
263
293
else :
264
- result .append ((e . file , - 1 , 'note' , 'In class "{}":' .format (
294
+ result .append ((file , - 1 , 'note' , 'In class "{}":' .format (
265
295
e .type )))
266
296
else :
267
297
if e .type is None :
268
- result .append ((e . file , - 1 , 'note' ,
298
+ result .append ((file , - 1 , 'note' ,
269
299
'In function "{}":' .format (
270
300
e .function_or_member )))
271
301
else :
272
- result .append ((e . file , - 1 , 'note' ,
302
+ result .append ((file , - 1 , 'note' ,
273
303
'In member "{}" of class "{}":' .format (
274
304
e .function_or_member , e .type )))
275
305
elif e .type != prev_type :
276
306
if e .type is None :
277
- result .append ((e . file , - 1 , 'note' , 'At top level:' ))
307
+ result .append ((file , - 1 , 'note' , 'At top level:' ))
278
308
else :
279
- result .append ((e . file , - 1 , 'note' ,
309
+ result .append ((file , - 1 , 'note' ,
280
310
'In class "{}":' .format (e .type )))
281
311
282
- result .append ((e . file , e .line , e .severity , e .message ))
312
+ result .append ((file , e .line , e .severity , e .message ))
283
313
284
314
prev_import_context = e .import_ctx
285
315
prev_function_or_member = e .function_or_member
0 commit comments