-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Flush error messages incrementally after processing a file #4396
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
Changes from 1 commit
4b38029
76f945f
310611d
059de14
c9a5a96
76bb7f8
5c9abec
844d5ca
c9d2355
699ba0b
376982d
caa8477
92dfc2d
cc64198
548078c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
In order to avoid duplicate error messages for errors produced in both load_graph() and process_graph() and to prevent misordered error messages in a number of places, lists of error messages are now tracked per-file. These lists are collected and printed out when a file is complete. To maintain consistency with clients that use .messages() (namely, tests), messages are generated file-at-a-time even when not printing them out incrementally. Fixes #1294
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -90,8 +90,16 @@ class Errors: | |
current error context (nested imports). | ||
""" | ||
|
||
# List of generated error messages. | ||
error_info = None # type: List[ErrorInfo] | ||
# Map from files to generated error messages. Is an OrderedDict so | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if it would be safer to use the module ID rather than the file as the key? Because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not all error infos have a module, unfortunately. |
||
# that it can be used to order messages based on the order the | ||
# files were processed. | ||
error_info_map = None # type: Dict[str, List[ErrorInfo]] | ||
|
||
# The size of error_info the last time that error messages were flushed | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no But perhaps the flushed errors are not used any more (other than being counted, and that's just used as a Boolean)? That might simplify things a bit? (It seems
Sorry, something went wrong. All reactions |
||
new_errors_start_map = None # type: Dict[str, int] | ||
|
||
# A cache of the formatted messages | ||
formatted_messages = None # type: List[str] | ||
|
||
# Current error context: nested import context/stack, as a list of (path, line) pairs. | ||
import_ctx = None # type: List[Tuple[str, int]] | ||
|
@@ -141,8 +149,10 @@ def __init__(self, show_error_context: bool = False, | |
self.initialize() | ||
|
||
def initialize(self) -> None: | ||
self.error_info = [] | ||
self.error_info_map = OrderedDict() | ||
self.new_errors_start_map = defaultdict(int) | ||
self.import_ctx = [] | ||
self.formatted_messages = [] | ||
self.error_files = set() | ||
self.type_name = [None] | ||
self.function_or_member = [None] | ||
|
@@ -289,6 +299,11 @@ def report(self, | |
target=self.current_target()) | ||
self.add_error_info(info) | ||
|
||
def _add_error_info(self, info: ErrorInfo) -> None: | ||
if info.file not in self.error_info_map: | ||
self.error_info_map[info.file] = [] | ||
self.error_info_map[info.file].append(info) | ||
|
||
def add_error_info(self, info: ErrorInfo) -> None: | ||
(file, line) = cast(Tuple[str, int], info.origin) # see issue 1855 | ||
if not info.blocker: # Blockers cannot be ignored | ||
|
@@ -302,40 +317,41 @@ def add_error_info(self, info: ErrorInfo) -> None: | |
if info.message in self.only_once_messages: | ||
return | ||
self.only_once_messages.add(info.message) | ||
self.error_info.append(info) | ||
self._add_error_info(info) | ||
self.error_files.add(file) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The whole variable
Sorry, something went wrong. All reactions |
||
|
||
def generate_unused_ignore_notes(self) -> None: | ||
for file, ignored_lines in self.ignored_lines.items(): | ||
if not self.is_typeshed_file(file): | ||
for line in ignored_lines - self.used_ignored_lines[file]: | ||
# Don't use report since add_error_info will ignore the error! | ||
info = ErrorInfo(self.import_context(), file, self.current_module(), None, | ||
None, line, -1, 'note', "unused 'type: ignore' comment", | ||
False, False) | ||
self.error_info.append(info) | ||
def generate_unused_ignore_notes(self, file: str) -> None: | ||
ignored_lines = self.ignored_lines[file] | ||
if not self.is_typeshed_file(file): | ||
for line in ignored_lines - self.used_ignored_lines[file]: | ||
# Don't use report since add_error_info will ignore the error! | ||
info = ErrorInfo(self.import_context(), file, self.current_module(), None, | ||
None, line, -1, 'note', "unused 'type: ignore' comment", | ||
False, False) | ||
self._add_error_info(info) | ||
|
||
def is_typeshed_file(self, file: str) -> bool: | ||
# gross, but no other clear way to tell | ||
return 'typeshed' in os.path.normpath(file).split(os.sep) | ||
|
||
def num_messages(self) -> int: | ||
"""Return the number of generated messages.""" | ||
return len(self.error_info) | ||
return sum(len(x) for x in self.error_info_map.values()) | ||
|
||
def is_errors(self) -> bool: | ||
"""Are there any generated errors?""" | ||
return bool(self.error_info) | ||
return bool(self.error_info_map) | ||
|
||
def is_blockers(self) -> bool: | ||
"""Are the any errors that are blockers?""" | ||
return any(err for err in self.error_info if err.blocker) | ||
return any(err for errs in self.error_info_map.values() for err in errs if err.blocker) | ||
|
||
def blocker_module(self) -> Optional[str]: | ||
"""Return the module with a blocking error, or None if not possible.""" | ||
for err in self.error_info: | ||
if err.blocker: | ||
return err.module | ||
for errs in self.error_info_map.values(): | ||
for err in errs: | ||
if err.blocker: | ||
return err.module | ||
return None | ||
|
||
def is_errors_for_file(self, file: str) -> bool: | ||
|
@@ -347,17 +363,22 @@ def raise_error(self) -> None: | |
|
||
Render the messages suitable for displaying. | ||
""" | ||
# self.new_messages() will format all messages that haven't already | ||
# been returned from a new_module_messages() call. Count how many | ||
# we've seen before that. | ||
already_seen = len(self.formatted_messages) | ||
raise CompileError(self.messages(), | ||
use_stdout=True, | ||
module_with_blocker=self.blocker_module()) | ||
module_with_blocker=self.blocker_module(), | ||
num_already_seen=already_seen) | ||
|
||
def messages(self) -> List[str]: | ||
def format_messages(self, error_info: List[ErrorInfo]) -> List[str]: | ||
"""Return a string list that represents the error messages. | ||
|
||
Use a form suitable for displaying to the user. | ||
""" | ||
a = [] # type: List[str] | ||
errors = self.render_messages(self.sort_messages(self.error_info)) | ||
errors = self.render_messages(self.sort_messages(error_info)) | ||
errors = self.remove_duplicates(errors) | ||
for file, line, column, severity, message in errors: | ||
s = '' | ||
|
@@ -375,12 +396,48 @@ def messages(self) -> List[str]: | |
a.append(s) | ||
return a | ||
|
||
def new_file_messages(self, path: str) -> List[str]: | ||
"""Return a string list of new error messages from a given file. | ||
|
||
Use a form suitable for displaying to the user. | ||
Formatted messages are cached in the order they are generated | ||
by new_file_messages() in order to have consistency in output | ||
between incrementally generated messages and .messages() calls. | ||
""" | ||
if path not in self.error_info_map: | ||
return [] | ||
msgs = self.format_messages(self.error_info_map[path][self.new_errors_start_map[path]:]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The RHS expression is a bit complicated. I'd split off Another concern I have here is that it's fairly subtle that
Sorry, something went wrong. All reactions |
||
self.new_errors_start_map[path] = len(self.error_info_map[path]) | ||
self.formatted_messages += msgs | ||
return msgs | ||
|
||
def new_messages(self) -> List[str]: | ||
"""Return a string list of new error messages. | ||
|
||
Use a form suitable for displaying to the user. | ||
Errors from different files are ordered based on the order in which | ||
they first generated an error. | ||
""" | ||
msgs = [] | ||
for key in self.error_info_map.keys(): | ||
msgs.extend(self.new_file_messages(key)) | ||
return msgs | ||
|
||
def messages(self) -> List[str]: | ||
"""Return a string list that represents the error messages. | ||
|
||
Use a form suitable for displaying to the user. | ||
""" | ||
self.new_messages() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add comment about
Sorry, something went wrong. All reactions |
||
return self.formatted_messages | ||
|
||
def targets(self) -> Set[str]: | ||
"""Return a set of all targets that contain errors.""" | ||
# TODO: Make sure that either target is always defined or that not being defined | ||
# is okay for fine-grained incremental checking. | ||
return set(info.target | ||
for info in self.error_info | ||
for errs in self.error_info_map.values() | ||
for info in errs | ||
if info.target) | ||
|
||
def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str], int, int, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GitHub won't let me place this comment on the line to which it applies -- the docstring for
Sorry, something went wrong. All reactions |
||
|
@@ -517,15 +574,18 @@ class CompileError(Exception): | |
use_stdout = False | ||
# Can be set in case there was a module with a blocking error | ||
module_with_blocker = None # type: Optional[str] | ||
num_already_seen = 0 | ||
|
||
def __init__(self, | ||
messages: List[str], | ||
use_stdout: bool = False, | ||
module_with_blocker: Optional[str] = None) -> None: | ||
module_with_blocker: Optional[str] = None, | ||
num_already_seen: int = 0) -> None: | ||
super().__init__('\n'.join(messages)) | ||
self.messages = messages | ||
self.use_stdout = use_stdout | ||
self.module_with_blocker = module_with_blocker | ||
self.num_already_seen = num_already_seen | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This adds another ugly wart to the API (see the code in build.py that uses this variable). It might be prettier to have another
Sorry, something went wrong. All reactions |
||
|
||
|
||
class DecodeError(Exception): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ | |
import sys | ||
import time | ||
|
||
from typing import Any, Dict, List, Mapping, Optional, Sequence, Set, Tuple | ||
from typing import Any, Dict, List, Mapping, Optional, Sequence, Set, Tuple, Callable | ||
|
||
from mypy import build | ||
from mypy import defaults | ||
|
@@ -62,13 +62,27 @@ def main(script_path: Optional[str], args: Optional[List[str]] = None) -> None: | |
args = sys.argv[1:] | ||
sources, options = process_options(args) | ||
serious = False | ||
errors = False | ||
|
||
def flush_errors(a: List[str]) -> None: | ||
nonlocal errors | ||
if a: | ||
errors = True | ||
f = sys.stderr if serious else sys.stdout | ||
try: | ||
for m in a: | ||
f.write(m + '\n') | ||
except BrokenPipeError: | ||
pass | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd
Sorry, something went wrong. All reactions |
||
|
||
try: | ||
res = type_check_only(sources, bin_dir, options) | ||
res = type_check_only(sources, bin_dir, options, flush_errors) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer if the main body of test cases (
Sorry, something went wrong. All reactions |
||
a = res.errors | ||
except CompileError as e: | ||
a = e.messages | ||
if not e.use_stdout: | ||
serious = True | ||
flush_errors(a[e.num_already_seen:]) | ||
if options.warn_unused_configs and options.unused_configs: | ||
print("Warning: unused section(s) in %s: %s" % | ||
(options.config_file, | ||
|
@@ -77,13 +91,7 @@ def main(script_path: Optional[str], args: Optional[List[str]] = None) -> None: | |
if options.junit_xml: | ||
t1 = time.time() | ||
util.write_junit_xml(t1 - t0, serious, a, options.junit_xml) | ||
if a: | ||
f = sys.stderr if serious else sys.stdout | ||
try: | ||
for m in a: | ||
f.write(m + '\n') | ||
except BrokenPipeError: | ||
pass | ||
if errors: | ||
sys.exit(1) | ||
|
||
|
||
|
@@ -112,11 +120,13 @@ def readlinkabs(link: str) -> str: | |
|
||
|
||
def type_check_only(sources: List[BuildSource], bin_dir: Optional[str], | ||
options: Options) -> BuildResult: | ||
options: Options, | ||
flush_errors: Optional[Callable[[List[str]], None]]) -> BuildResult: | ||
# Type-check the program and dependencies. | ||
return build.build(sources=sources, | ||
bin_dir=bin_dir, | ||
options=options) | ||
options=options, | ||
flush_errors=flush_errors) | ||
|
||
|
||
FOOTER = """environment variables: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you've made this effectively into a per-module option, please add it to the list of such in options.py.