Skip to content

Commit bf47c65

Browse files
authored
Add beginnings of error code support (#7267)
This PR adds a foundation for error codes and implements a few error codes. It also adds support for `# type: ignore[code1, ...]` which ignores only specific error codes on a line. Only a few errors include interesting error codes at this point. I'll add support for more error codes in additional PRs. Most errors will implicitly fall back to a `misc` error code. Error codes are only shown if `--show-error-codes` is used. The error codes look like this in mypy output: ``` t.py:3: error: "str" has no attribute "trim" [attr-defined] ``` Error codes are intended to be short but human-readable. The name of an error code refers to the check that produces this error. In the above example we generate a "no attribute" error when we check whether an attribute is defined. Work towards #7239.
1 parent 1d9024b commit bf47c65

13 files changed

+390
-150
lines changed

mypy/build.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,9 @@ def _build(sources: List[BuildSource],
199199
reports = Reports(data_dir, options.report_dirs)
200200

201201
source_set = BuildSourceSet(sources)
202-
errors = Errors(options.show_error_context, options.show_column_numbers)
202+
errors = Errors(options.show_error_context,
203+
options.show_column_numbers,
204+
options.show_error_codes)
203205
plugin, snapshot = load_plugins(options, errors, stdout)
204206

205207
# Construct a build manager object to hold state during the build.

mypy/errorcodes.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Classification of possible errors mypy can detect.
2+
3+
These can be used for filtering specific errors.
4+
"""
5+
6+
from typing import List
7+
from typing_extensions import Final
8+
9+
10+
# All created error codes are implicitly stored in this list.
11+
all_error_codes = [] # type: List[ErrorCode]
12+
13+
14+
class ErrorCode:
15+
def __init__(self, code: str, description: str, category: str) -> None:
16+
self.code = code
17+
self.description = description
18+
self.category = category
19+
20+
def __str__(self) -> str:
21+
return '<ErrorCode {}>'.format(self.code)
22+
23+
24+
ATTR_DEFINED = ErrorCode(
25+
'attr-defined', "Check that attribute exists", 'General') # type: Final
26+
NAME_DEFINED = ErrorCode(
27+
'name-defined', "Check that name is defined", 'General') # type: Final
28+
CALL_ARG = ErrorCode(
29+
'call-arg', "Check number, names and kinds of arguments in calls", 'General') # type: Final
30+
ARG_TYPE = ErrorCode(
31+
'arg-type', "Check argument types in calls", 'General') # type: Final
32+
VALID_TYPE = ErrorCode(
33+
'valid-type', "Check that type (annotation) is valid", 'General') # type: Final
34+
MISSING_ANN = ErrorCode(
35+
'var-annotated', "Require variable annotation if type can't be inferred",
36+
'General') # type: Final
37+
OVERRIDE = ErrorCode(
38+
'override', "Check that method override is compatible with base class",
39+
'General') # type: Final
40+
RETURN_VALUE = ErrorCode(
41+
'return-value', "Check that return value is compatible with signature",
42+
'General') # type: Final
43+
ASSIGNMENT = ErrorCode(
44+
'assignment', "Check that assigned value is compatible with target", 'General') # type: Final
45+
46+
SYNTAX = ErrorCode(
47+
'syntax', "Report syntax errors", 'General') # type: Final
48+
49+
MISC = ErrorCode(
50+
'misc', "Miscenallenous other checks", 'General') # type: Final

mypy/errors.py

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from mypy.scope import Scope
1010
from mypy.options import Options
1111
from mypy.version import __version__ as mypy_version
12+
from mypy.errorcodes import ErrorCode
13+
from mypy import errorcodes as codes
1214

1315
T = TypeVar('T')
1416
allowed_duplicates = ['@overload', 'Got:', 'Expected:'] # type: Final
@@ -45,6 +47,9 @@ class ErrorInfo:
4547
# The error message.
4648
message = ''
4749

50+
# The error code.
51+
code = None # type: Optional[ErrorCode]
52+
4853
# If True, we should halt build after the file that generated this error.
4954
blocker = False
5055

@@ -68,6 +73,7 @@ def __init__(self,
6873
column: int,
6974
severity: str,
7075
message: str,
76+
code: Optional[ErrorCode],
7177
blocker: bool,
7278
only_once: bool,
7379
origin: Optional[Tuple[str, int, int]] = None,
@@ -81,12 +87,23 @@ def __init__(self,
8187
self.column = column
8288
self.severity = severity
8389
self.message = message
90+
self.code = code
8491
self.blocker = blocker
8592
self.only_once = only_once
8693
self.origin = origin or (file, line, line)
8794
self.target = target
8895

8996

97+
# Type used internally to represent errors:
98+
# (path, line, column, severity, message, code)
99+
ErrorTuple = Tuple[Optional[str],
100+
int,
101+
int,
102+
str,
103+
str,
104+
Optional[ErrorCode]]
105+
106+
90107
class Errors:
91108
"""Container for compile errors.
92109
@@ -111,8 +128,9 @@ class Errors:
111128
# Path to current file.
112129
file = '' # type: str
113130

114-
# Ignore errors on these lines of each file.
115-
ignored_lines = None # type: Dict[str, Set[int]]
131+
# Ignore some errors on these lines of each file
132+
# (path -> line -> error-codes)
133+
ignored_lines = None # type: Dict[str, Dict[int, List[str]]]
116134

117135
# Lines on which an error was actually ignored.
118136
used_ignored_lines = None # type: Dict[str, Set[int]]
@@ -135,10 +153,13 @@ class Errors:
135153
target_module = None # type: Optional[str]
136154
scope = None # type: Optional[Scope]
137155

138-
def __init__(self, show_error_context: bool = False,
139-
show_column_numbers: bool = False) -> None:
156+
def __init__(self,
157+
show_error_context: bool = False,
158+
show_column_numbers: bool = False,
159+
show_error_codes: bool = False) -> None:
140160
self.show_error_context = show_error_context
141161
self.show_column_numbers = show_column_numbers
162+
self.show_error_codes = show_error_codes
142163
self.initialize()
143164

144165
def initialize(self) -> None:
@@ -197,7 +218,7 @@ def set_file(self, file: str,
197218
self.scope = scope
198219

199220
def set_file_ignored_lines(self, file: str,
200-
ignored_lines: Set[int],
221+
ignored_lines: Dict[int, List[str]],
201222
ignore_all: bool = False) -> None:
202223
self.ignored_lines[file] = ignored_lines
203224
if ignore_all:
@@ -226,6 +247,8 @@ def report(self,
226247
line: int,
227248
column: Optional[int],
228249
message: str,
250+
code: Optional[ErrorCode] = None,
251+
*,
229252
blocker: bool = False,
230253
severity: str = 'error',
231254
file: Optional[str] = None,
@@ -237,7 +260,9 @@ def report(self,
237260
238261
Args:
239262
line: line number of error
263+
column: column number of error
240264
message: message to report
265+
code: error code (defaults to 'misc' for 'error' severity)
241266
blocker: if True, don't continue analysis after this error
242267
severity: 'error' or 'note'
243268
file: if non-None, override current file as context
@@ -267,8 +292,11 @@ def report(self,
267292
if end_line is None:
268293
end_line = origin_line
269294

295+
if severity == 'error' and code is None:
296+
code = codes.MISC
297+
270298
info = ErrorInfo(self.import_context(), file, self.current_module(), type,
271-
function, line, column, severity, message,
299+
function, line, column, severity, message, code,
272300
blocker, only_once,
273301
origin=(self.file, origin_line, end_line),
274302
target=self.current_target())
@@ -293,7 +321,7 @@ def add_error_info(self, info: ErrorInfo) -> None:
293321
# Check each line in this context for "type: ignore" comments.
294322
# line == end_line for most nodes, so we only loop once.
295323
for scope_line in range(line, end_line + 1):
296-
if scope_line in self.ignored_lines[file]:
324+
if self.is_ignored_error(scope_line, info, self.ignored_lines[file]):
297325
# Annotation requests us to ignore all errors on this line.
298326
self.used_ignored_lines[file].add(scope_line)
299327
return
@@ -305,6 +333,16 @@ def add_error_info(self, info: ErrorInfo) -> None:
305333
self.only_once_messages.add(info.message)
306334
self._add_error_info(file, info)
307335

336+
def is_ignored_error(self, line: int, info: ErrorInfo, ignores: Dict[int, List[str]]) -> bool:
337+
if line not in ignores:
338+
return False
339+
elif not ignores[line]:
340+
# Empty list means that we ignore all errors
341+
return True
342+
elif info.code:
343+
return info.code.code in ignores[line]
344+
return False
345+
308346
def clear_errors_in_targets(self, path: str, targets: Set[str]) -> None:
309347
"""Remove errors in specific fine-grained targets within a file."""
310348
if path in self.error_info_map:
@@ -319,11 +357,11 @@ def clear_errors_in_targets(self, path: str, targets: Set[str]) -> None:
319357
def generate_unused_ignore_errors(self, file: str) -> None:
320358
ignored_lines = self.ignored_lines[file]
321359
if not self.is_typeshed_file(file) and file not in self.ignored_files:
322-
for line in ignored_lines - self.used_ignored_lines[file]:
360+
for line in set(ignored_lines) - self.used_ignored_lines[file]:
323361
# Don't use report since add_error_info will ignore the error!
324362
info = ErrorInfo(self.import_context(), file, self.current_module(), None,
325363
None, line, -1, 'error', "unused 'type: ignore' comment",
326-
False, False)
364+
None, False, False)
327365
self._add_error_info(file, info)
328366

329367
def is_typeshed_file(self, file: str) -> bool:
@@ -373,7 +411,7 @@ def format_messages(self, error_info: List[ErrorInfo]) -> List[str]:
373411
a = [] # type: List[str]
374412
errors = self.render_messages(self.sort_messages(error_info))
375413
errors = self.remove_duplicates(errors)
376-
for file, line, column, severity, message in errors:
414+
for file, line, column, severity, message, code in errors:
377415
s = ''
378416
if file is not None:
379417
if self.show_column_numbers and line >= 0 and column >= 0:
@@ -385,6 +423,8 @@ def format_messages(self, error_info: List[ErrorInfo]) -> List[str]:
385423
s = '{}: {}: {}'.format(srcloc, severity, message)
386424
else:
387425
s = message
426+
if self.show_error_codes and code:
427+
s = '{} [{}]'.format(s, code.code)
388428
a.append(s)
389429
return a
390430

@@ -420,18 +460,16 @@ def targets(self) -> Set[str]:
420460
for info in errs
421461
if info.target)
422462

423-
def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str], int, int,
424-
str, str]]:
463+
def render_messages(self,
464+
errors: List[ErrorInfo]) -> List[ErrorTuple]:
425465
"""Translate the messages into a sequence of tuples.
426466
427-
Each tuple is of form (path, line, col, severity, message).
467+
Each tuple is of form (path, line, col, severity, message, code).
428468
The rendered sequence includes information about error contexts.
429469
The path item may be None. If the line item is negative, the
430470
line number is not defined for the tuple.
431471
"""
432-
result = [] # type: List[Tuple[Optional[str], int, int, str, str]]
433-
# (path, line, column, severity, message)
434-
472+
result = [] # type: List[ErrorTuple]
435473
prev_import_context = [] # type: List[Tuple[str, int]]
436474
prev_function_or_member = None # type: Optional[str]
437475
prev_type = None # type: Optional[str]
@@ -455,7 +493,7 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str],
455493
# Remove prefix to ignore from path (if present) to
456494
# simplify path.
457495
path = remove_path_prefix(path, self.ignore_prefix)
458-
result.append((None, -1, -1, 'note', fmt.format(path, line)))
496+
result.append((None, -1, -1, 'note', fmt.format(path, line), None))
459497
i -= 1
460498

461499
file = self.simplify_path(e.file)
@@ -467,27 +505,27 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str],
467505
e.type != prev_type):
468506
if e.function_or_member is None:
469507
if e.type is None:
470-
result.append((file, -1, -1, 'note', 'At top level:'))
508+
result.append((file, -1, -1, 'note', 'At top level:', None))
471509
else:
472510
result.append((file, -1, -1, 'note', 'In class "{}":'.format(
473-
e.type)))
511+
e.type), None))
474512
else:
475513
if e.type is None:
476514
result.append((file, -1, -1, 'note',
477515
'In function "{}":'.format(
478-
e.function_or_member)))
516+
e.function_or_member), None))
479517
else:
480518
result.append((file, -1, -1, 'note',
481519
'In member "{}" of class "{}":'.format(
482-
e.function_or_member, e.type)))
520+
e.function_or_member, e.type), None))
483521
elif e.type != prev_type:
484522
if e.type is None:
485-
result.append((file, -1, -1, 'note', 'At top level:'))
523+
result.append((file, -1, -1, 'note', 'At top level:', None))
486524
else:
487525
result.append((file, -1, -1, 'note',
488-
'In class "{}":'.format(e.type)))
526+
'In class "{}":'.format(e.type), None))
489527

490-
result.append((file, e.line, e.column, e.severity, e.message))
528+
result.append((file, e.line, e.column, e.severity, e.message, e.code))
491529

492530
prev_import_context = e.import_ctx
493531
prev_function_or_member = e.function_or_member
@@ -518,10 +556,9 @@ def sort_messages(self, errors: List[ErrorInfo]) -> List[ErrorInfo]:
518556
result.extend(a)
519557
return result
520558

521-
def remove_duplicates(self, errors: List[Tuple[Optional[str], int, int, str, str]]
522-
) -> List[Tuple[Optional[str], int, int, str, str]]:
559+
def remove_duplicates(self, errors: List[ErrorTuple]) -> List[ErrorTuple]:
523560
"""Remove duplicates from a sorted error list."""
524-
res = [] # type: List[Tuple[Optional[str], int, int, str, str]]
561+
res = [] # type: List[ErrorTuple]
525562
i = 0
526563
while i < len(errors):
527564
dup = False

0 commit comments

Comments
 (0)