Skip to content

Commit ca2912b

Browse files
committed
WIP: Warn on unused # type: ignores
Still needs tests.
1 parent cb1786f commit ca2912b

File tree

5 files changed

+39
-20
lines changed

5 files changed

+39
-20
lines changed

mypy/build.py

+4
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
CHECK_UNTYPED_DEFS = 'check-untyped-defs'
6969
# Also check typeshed for missing annotations
7070
WARN_INCOMPLETE_STUB = 'warn-incomplete-stub'
71+
# Warn about unused '# type: ignore' comments
72+
WARN_UNUSED_IGNORES = 'warn-unused-ignores'
7173

7274
PYTHON_EXTENSIONS = ['.pyi', '.py']
7375

@@ -1334,6 +1336,8 @@ def dispatch(sources: List[BuildSource], manager: BuildManager) -> None:
13341336
graph = load_graph(sources, manager)
13351337
manager.log("Loaded graph with %d nodes" % len(graph))
13361338
process_graph(graph, manager)
1339+
if WARN_UNUSED_IGNORES in manager.flags:
1340+
manager.errors.generate_unused_ignore_notes()
13371341

13381342

13391343
def load_graph(sources: List[BuildSource], manager: BuildManager) -> Graph:

mypy/checker.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,7 @@ def visit_file(self, file_node: MypyFile, path: str) -> None:
416416
"""Type check a mypy file with the given path."""
417417
self.pass_num = 0
418418
self.is_stub = file_node.is_stub
419-
self.errors.set_file(path)
420-
self.errors.set_ignored_lines(file_node.ignored_lines)
419+
self.errors.set_file(path, file_node.ignored_lines)
421420
self.globals = file_node.names
422421
self.weak_opts = file_node.weak_opts
423422
self.enter_partial_types()
@@ -432,7 +431,6 @@ def visit_file(self, file_node: MypyFile, path: str) -> None:
432431
if self.deferred_nodes:
433432
self.check_second_pass()
434433

435-
self.errors.set_ignored_lines(set())
436434
self.current_node_deferred = False
437435

438436
all_ = self.globals.get('__all__')
@@ -1522,7 +1520,7 @@ def set_inference_error_fallback_type(self, var: Var, lvalue: Node, type: Type,
15221520
15231521
We implement this here by giving x a valid type (Any).
15241522
"""
1525-
if context.get_line() in self.errors.ignored_lines:
1523+
if context.get_line() in self.errors.ignored_lines[self.errors.file]:
15261524
self.set_inferred_type(var, lvalue, AnyType())
15271525

15281526
def narrow_type_from_binder(self, expr: Node, known_type: Type) -> Type:

mypy/errors.py

+26-9
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
import os.path
33
import sys
44
import 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

910
T = 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)

mypy/main.py

+5
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ def parse_version(v):
153153
parser.add_argument('--warn-incomplete-stub', action='store_true',
154154
help="warn if missing type annotation in typeshed, only relevant with"
155155
" --check-untyped-defs enabled")
156+
parser.add_argument('--warn-unused-ignores', action='store_true',
157+
help="warn about unneeded '# type: ignore' comments")
156158
parser.add_argument('--fast-parser', action='store_true',
157159
help="enable experimental fast parser")
158160
parser.add_argument('-i', '--incremental', action='store_true',
@@ -252,6 +254,9 @@ def parse_version(v):
252254
if args.warn_incomplete_stub:
253255
options.build_flags.append(build.WARN_INCOMPLETE_STUB)
254256

257+
if args.warn_unused_ignores:
258+
options.build_flags.append(build.WARN_UNUSED_IGNORES)
259+
255260
# experimental
256261
if args.fast_parser:
257262
options.build_flags.append(build.FAST_PARSER)

mypy/semanal.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,7 @@ def __init__(self,
219219
self.all_exports = set() # type: Set[str]
220220

221221
def visit_file(self, file_node: MypyFile, fnam: str) -> None:
222-
self.errors.set_file(fnam)
223-
self.errors.set_ignored_lines(file_node.ignored_lines)
222+
self.errors.set_file(fnam, file_node.ignored_lines)
224223
self.cur_mod_node = file_node
225224
self.cur_mod_id = file_node.fullname()
226225
self.is_stub_file = fnam.lower().endswith('.pyi')
@@ -243,8 +242,6 @@ def visit_file(self, file_node: MypyFile, fnam: str) -> None:
243242
if self.cur_mod_id == 'builtins':
244243
remove_imported_names_from_symtable(self.globals, 'builtins')
245244

246-
self.errors.set_ignored_lines(set())
247-
248245
if '__all__' in self.globals:
249246
for name, g in self.globals.items():
250247
if name not in self.all_exports:
@@ -2469,10 +2466,8 @@ def __init__(self, modules: Dict[str, MypyFile], errors: Errors) -> None:
24692466
self.errors = errors
24702467

24712468
def visit_file(self, file_node: MypyFile, fnam: str) -> None:
2472-
self.errors.set_file(fnam)
2473-
self.errors.set_ignored_lines(file_node.ignored_lines)
2469+
self.errors.set_file(fnam, file_node.ignored_lines)
24742470
self.accept(file_node)
2475-
self.errors.set_ignored_lines(set())
24762471

24772472
def accept(self, node: Node) -> None:
24782473
try:

0 commit comments

Comments
 (0)