From adfe03bf41e9372eb7195fec4079609c246f6813 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Thu, 26 Apr 2018 10:50:26 -0700 Subject: [PATCH] Fix crashes in coverage reports There are two parts to this: 1. Look harder for the start of a function, since decorators can make it tricky to find. 2. Be more defensive about handling dodgy line number info. Our line numbers aren't reliable enough to be crashing the reporter over. This fixes a crash related to attrs. Fixes #4563 --- mypy/report.py | 23 ++++++++++++++++++++--- test-data/unit/reports.test | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/mypy/report.py b/mypy/report.py index 3e49ed29a47c..b5d37b128b37 100644 --- a/mypy/report.py +++ b/mypy/report.py @@ -295,7 +295,20 @@ def indentation_level(self, line_number: int) -> Optional[int]: def visit_func_def(self, defn: FuncDef) -> None: start_line = defn.get_line() - 1 - start_indent = self.indentation_level(start_line) + start_indent = None + # When a function is decorated, sometimes the start line will point to + # whitespace or comments between the decorator and the function, so + # we have to look for the start. + while start_line < len(self.source): + start_indent = self.indentation_level(start_line) + if start_indent is not None: + break + start_line += 1 + # If we can't find the function give up and don't annotate anything. + # Our line numbers are not reliable enough to be asserting on. + if start_indent is None: + return + cur_line = start_line + 1 end_line = cur_line # After this loop, function body will be lines [start_line, end_line) @@ -315,8 +328,12 @@ def visit_func_def(self, defn: FuncDef) -> None: is_typed = defn.type is not None for line in range(start_line, end_line): old_indent, _ = self.lines_covered[line] - assert start_indent is not None and start_indent > old_indent - self.lines_covered[line] = (start_indent, is_typed) + # If there was an old indent level for this line, and the new + # level isn't increasing the indentation, ignore it. + # This is to be defensive against funniness in our line numbers, + # which are not always reliable. + if old_indent <= start_indent: + self.lines_covered[line] = (start_indent, is_typed) # Visit the body, in case there are nested functions super().visit_func_def(defn) diff --git a/test-data/unit/reports.test b/test-data/unit/reports.test index bb0f4f55c7c9..11a4dc060880 100644 --- a/test-data/unit/reports.test +++ b/test-data/unit/reports.test @@ -337,3 +337,23 @@ def foo(): n 0 0 0 0 0 0 0 ----------------------------------------------------------------------------------------------------------------- Total 0 0 0 0 0 0 0 +[case testTrickyCoverage] +# cmd: mypy --linecoverage-report=report n.py +[file n.py] +import attr + +def blah(x): return x + +@blah + +def f(x: int) -> None: pass + +class Foo: + @blah + #hi + def f(self, x: int) -> None: + pass + +@attr.s +class Z(object): + pass