Skip to content

Add flag to tag known messages with an error code (take 2) #6472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ def _build(sources: List[BuildSource],
reports = Reports(data_dir, options.report_dirs)

source_set = BuildSourceSet(sources)
errors = Errors(options.show_error_context, options.show_column_numbers)
errors = Errors(options.show_error_context, options.show_column_numbers,
options.show_error_codes)
errors.initialize_error_codes()
plugin, snapshot = load_plugins(options, errors)

# Construct a build manager object to hold state during the build.
Expand Down Expand Up @@ -327,18 +329,33 @@ def load_plugins(options: Options, errors: Errors) -> Tuple[Plugin, Dict[str, st
import importlib
snapshot = {} # type: Dict[str, str]

def plugin_error(message: str) -> None:
errors.report(line, 0, message)
errors.raise_error()

def load_plugin(plugin: Plugin, module_name: str) -> None:
try:
error_codes = plugin.get_error_codes()
except Exception:
plugin_error('Error calling get_error_codes of {}'.format(module_name))
raise

try:
errors.register_error_codes(module_name, error_codes)
except Exception:
plugin_error('Error processing result of get_error_codes ({})'.format(module_name))
raise

default_plugin = DefaultPlugin(options) # type: Plugin
load_plugin(default_plugin, 'mypy.plugins.default')

if not options.config_file:
return default_plugin, snapshot

line = find_config_file_line_number(options.config_file, 'mypy', 'plugins')
if line == -1:
line = 1 # We need to pick some line number that doesn't look too confusing

def plugin_error(message: str) -> None:
errors.report(line, 0, message)
errors.raise_error()

custom_plugins = [] # type: List[Plugin]
errors.set_file(options.config_file, None)
for plugin_path in options.plugins:
Expand Down Expand Up @@ -392,11 +409,15 @@ def plugin_error(message: str) -> None:
'Return value of "plugin" must be a subclass of "mypy.plugin.Plugin" '
'(in {})'.format(plugin_path))
try:
custom_plugins.append(plugin_type(options))
snapshot[module_name] = take_module_snapshot(module)
plugin = plugin_type(options) # type: Plugin
except Exception:
print('Error constructing plugin instance of {}\n'.format(plugin_type.__name__))
raise # Propagate to display traceback
else:
load_plugin(plugin, module_name)
custom_plugins.append(plugin)
snapshot[module_name] = take_module_snapshot(module)

# Custom plugins take precedence over the default plugin.
return ChainedPlugin(options, custom_plugins + [default_plugin]), snapshot

Expand Down
1 change: 1 addition & 0 deletions mypy/dmypy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ def cmd_run(self, version: str, args: Sequence[str]) -> Dict[str, object]:
return {'out': '', 'err': str(err), 'status': 2}
except SystemExit as e:
return {'out': stdout.getvalue(), 'err': stderr.getvalue(), 'status': e.code}
assert sources is not None
return self.check(sources)

def cmd_check(self, files: Sequence[str]) -> Dict[str, object]:
Expand Down
101 changes: 77 additions & 24 deletions mypy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import traceback
from collections import OrderedDict, defaultdict

from typing import Tuple, List, TypeVar, Set, Dict, Optional
from typing import Tuple, List, TypeVar, Set, Dict, Optional, Any

from mypy.scope import Scope
from mypy.options import Options
Expand Down Expand Up @@ -60,6 +60,9 @@ class ErrorInfo:
# Fine-grained incremental target where this was reported
target = None # type: Optional[str]

# Error code identifying the message, for grouping/filtering
id = None # type: Optional[str]

def __init__(self,
import_ctx: List[Tuple[str, int]],
file: str,
Expand All @@ -73,7 +76,8 @@ def __init__(self,
blocker: bool,
only_once: bool,
origin: Optional[Tuple[str, int]] = None,
target: Optional[str] = None) -> None:
target: Optional[str] = None,
id: Optional[str] = None) -> None:
self.import_ctx = import_ctx
self.file = file
self.module = module
Expand All @@ -87,6 +91,7 @@ def __init__(self,
self.only_once = only_once
self.origin = origin or (file, line)
self.target = target
self.id = id


class Errors:
Expand Down Expand Up @@ -131,16 +136,24 @@ class Errors:
# Set to True to show column numbers in error messages.
show_column_numbers = False # type: bool

# Set to True to show error codes in error messages.
show_error_codes = False

# State for keeping track of the current fine-grained incremental mode target.
# (See mypy.server.update for more about targets.)
# Current module id.
target_module = None # type: Optional[str]
scope = None # type: Optional[Scope]

# Mapping of error string literal to code
error_codes = None # type: Dict[str, str]

def __init__(self, show_error_context: bool = False,
show_column_numbers: bool = False) -> None:
show_column_numbers: bool = False,
show_error_codes: bool = False) -> None:
self.show_error_context = show_error_context
self.show_column_numbers = show_column_numbers
self.show_error_codes = show_error_codes
self.initialize()

def initialize(self) -> None:
Expand All @@ -155,18 +168,20 @@ def initialize(self) -> None:
self.only_once_messages = set()
self.scope = None
self.target_module = None
self.error_codes = {}

def reset(self) -> None:
self.initialize()

def copy(self) -> 'Errors':
new = Errors(self.show_error_context, self.show_column_numbers)
new = Errors(self.show_error_context, self.show_column_numbers, self.show_error_codes)
new.file = self.file
new.import_ctx = self.import_ctx[:]
new.type_name = self.type_name[:]
new.function_or_member = self.function_or_member[:]
new.target_module = self.target_module
new.scope = self.scope
new.error_codes = self.error_codes.copy()
return new

def total_errors(self) -> int:
Expand Down Expand Up @@ -233,7 +248,8 @@ def report(self,
file: Optional[str] = None,
only_once: bool = False,
origin_line: Optional[int] = None,
offset: int = 0) -> None:
offset: int = 0,
format_args: Optional[Tuple[Any, ...]] = None) -> None:
"""Report message at the given line using the current error context.

Args:
Expand All @@ -244,6 +260,7 @@ def report(self,
file: if non-None, override current file as context
only_once: if True, only report this exact message once per build
origin_line: if non-None, override current context as origin
format_args: arguments to pass to message.format()
"""
if self.scope:
type = self.scope.current_type_name()
Expand All @@ -254,17 +271,24 @@ def report(self,
type = None
function = None

if self.show_error_codes:
msg_id = self.error_codes.get(message)
else:
msg_id = None

if column is None:
column = -1
if file is None:
file = self.file
if offset:
message = " " * offset + message
if format_args:
message = message.format(*format_args)
info = ErrorInfo(self.import_context(), file, self.current_module(), type,
function, line, column, severity, message,
blocker, only_once,
origin=(self.file, origin_line) if origin_line else None,
target=self.current_target())
target=self.current_target(), id=msg_id)
self.add_error_info(info)

def _add_error_info(self, file: str, info: ErrorInfo) -> None:
Expand Down Expand Up @@ -356,7 +380,7 @@ def format_messages(self, error_info: List[ErrorInfo]) -> List[str]:
a = [] # type: List[str]
errors = self.render_messages(self.sort_messages(error_info))
errors = self.remove_duplicates(errors)
for file, line, column, severity, message in errors:
for file, line, column, severity, message, id in errors:
s = ''
if file is not None:
if self.show_column_numbers and line is not None and line >= 0 \
Expand All @@ -366,7 +390,12 @@ def format_messages(self, error_info: List[ErrorInfo]) -> List[str]:
srcloc = '{}:{}'.format(file, line)
else:
srcloc = file
s = '{}: {}: {}'.format(srcloc, severity, message)
if self.show_error_codes:
s = '{}: {}: {}: {}'.format(srcloc, severity,
id if id is not None else 'uncategorized_error',
message)
else:
s = '{}: {}: {}'.format(srcloc, severity, message)
else:
s = message
a.append(s)
Expand Down Expand Up @@ -404,21 +433,22 @@ def targets(self) -> Set[str]:
for info in errs
if info.target)

def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str], int, int,
str, str]]:
def render_messages(self, errors: List[ErrorInfo]
) -> List[Tuple[Optional[str], int, int, str, str, Optional[str]]]:
"""Translate the messages into a sequence of tuples.

Each tuple is of form (path, line, col, severity, message).
The rendered sequence includes information about error contexts.
The path item may be None. If the line item is negative, the
line number is not defined for the tuple.
"""
result = [] # type: List[Tuple[Optional[str], int, int, str, str]]
result = [] # type: List[Tuple[Optional[str], int, int, str, str, Optional[str]]]
# (path, line, column, severity, message)

prev_import_context = [] # type: List[Tuple[str, int]]
prev_function_or_member = None # type: Optional[str]
prev_type = None # type: Optional[str]
prev_msg_id = None # type: Optional[str]

for e in errors:
# Report module import context, if different from previous message.
Expand All @@ -439,7 +469,7 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str],
# Remove prefix to ignore from path (if present) to
# simplify path.
path = remove_path_prefix(path, self.ignore_prefix)
result.append((None, -1, -1, 'note', fmt.format(path, line)))
result.append((None, -1, -1, 'note', fmt.format(path, line), prev_msg_id))
i -= 1

file = self.simplify_path(e.file)
Expand All @@ -451,31 +481,34 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str],
e.type != prev_type):
if e.function_or_member is None:
if e.type is None:
result.append((file, -1, -1, 'note', 'At top level:'))
result.append((file, -1, -1, 'note', 'At top level:', prev_msg_id))
else:
result.append((file, -1, -1, 'note', 'In class "{}":'.format(
e.type)))
result.append((file, -1, -1, 'note',
'In class "{}":'.format(e.type),
prev_msg_id))
else:
if e.type is None:
result.append((file, -1, -1, 'note',
'In function "{}":'.format(
e.function_or_member)))
'In function "{}":'.format(e.function_or_member),
prev_msg_id))
else:
result.append((file, -1, -1, 'note',
'In member "{}" of class "{}":'.format(
e.function_or_member, e.type)))
e.function_or_member, e.type),
prev_msg_id))
elif e.type != prev_type:
if e.type is None:
result.append((file, -1, -1, 'note', 'At top level:'))
result.append((file, -1, -1, 'note', 'At top level:', prev_msg_id))
else:
result.append((file, -1, -1, 'note',
'In class "{}":'.format(e.type)))
'In class "{}":'.format(e.type), prev_msg_id))

result.append((file, e.line, e.column, e.severity, e.message))
result.append((file, e.line, e.column, e.severity, e.message, e.id))

prev_import_context = e.import_ctx
prev_function_or_member = e.function_or_member
prev_type = e.type
prev_msg_id = e.id

return result

Expand All @@ -502,10 +535,11 @@ def sort_messages(self, errors: List[ErrorInfo]) -> List[ErrorInfo]:
result.extend(a)
return result

def remove_duplicates(self, errors: List[Tuple[Optional[str], int, int, str, str]]
) -> List[Tuple[Optional[str], int, int, str, str]]:
def remove_duplicates(self,
errors: List[Tuple[Optional[str], int, int, str, str, Optional[str]]]
) -> List[Tuple[Optional[str], int, int, str, str, Optional[str]]]:
"""Remove duplicates from a sorted error list."""
res = [] # type: List[Tuple[Optional[str], int, int, str, str]]
res = [] # type: List[Tuple[Optional[str], int, int, str, str, Optional[str]]]
i = 0
while i < len(errors):
dup = False
Expand All @@ -526,6 +560,25 @@ def remove_duplicates(self, errors: List[Tuple[Optional[str], int, int, str, str
i += 1
return res

def register_error_codes(self, namespace: str, error_codes: Dict[str, str]) -> None:
"""Add error codes and their message literals to the list of known errors.

Args:
namespace: used to indentify the source of the error code (usually a plugin)
error_codes: map of error code to message literal
"""
# reverse the lookup
updates = {msg: namespace + ':' + name for name, msg in error_codes.items()}
# FIXME: check for name clashes
self.error_codes.update(updates)

def initialize_error_codes(self) -> None:
"""Populate error codes"""
from mypy.plugins.common import extract_error_codes
import mypy.message_registry
# read native error codes from the message registry
self.register_error_codes('mypy', extract_error_codes(mypy.message_registry))


class CompileError(Exception):
"""Exception raised when there is a compile error.
Expand Down
3 changes: 3 additions & 0 deletions mypy/interpreted_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ def __init__(self, options: Options) -> None:
def set_modules(self, modules: Dict[str, MypyFile]) -> None:
self._modules = modules

def get_error_codes(self) -> Dict[str, str]:
return {}

def lookup_fully_qualified(self, fullname: str) -> Optional[SymbolTableNode]:
assert self._modules is not None
return lookup_fully_qualified(fullname, self._modules)
Expand Down
Loading