| 313 | |
| 314 | |
| 315 | class LineCoverageVisitor(TraverserVisitor): |
| 316 | def __init__(self, source: list[str]) -> None: |
| 317 | self.source = source |
| 318 | |
| 319 | # For each line of source, we maintain a pair of |
| 320 | # * the indentation level of the surrounding function |
| 321 | # (-1 if not inside a function), and |
| 322 | # * whether the surrounding function is typed. |
| 323 | # Initially, everything is covered at indentation level -1. |
| 324 | self.lines_covered = [(-1, True) for l in source] |
| 325 | |
| 326 | # The Python AST has position information for the starts of |
| 327 | # elements, but not for their ends. Fortunately the |
| 328 | # indentation-based syntax makes it pretty easy to find where a |
| 329 | # block ends without doing any real parsing. |
| 330 | |
| 331 | # TODO: Handle line continuations (explicit and implicit) and |
| 332 | # multi-line string literals. (But at least line continuations |
| 333 | # are normally more indented than their surrounding block anyways, |
| 334 | # by PEP 8.) |
| 335 | |
| 336 | def indentation_level(self, line_number: int) -> int | None: |
| 337 | """Return the indentation of a line of the source (specified by |
| 338 | zero-indexed line number). Returns None for blank lines or comments.""" |
| 339 | line = self.source[line_number] |
| 340 | indent = 0 |
| 341 | for char in list(line): |
| 342 | if char == " ": |
| 343 | indent += 1 |
| 344 | elif char == "\t": |
| 345 | indent = 8 * ((indent + 8) // 8) |
| 346 | elif char == "#": |
| 347 | # Line is a comment; ignore it |
| 348 | return None |
| 349 | elif char == "\n": |
| 350 | # Line is entirely whitespace; ignore it |
| 351 | return None |
| 352 | # TODO line continuation (\) |
| 353 | else: |
| 354 | # Found a non-whitespace character |
| 355 | return indent |
| 356 | # Line is entirely whitespace, and at end of file |
| 357 | # with no trailing newline; ignore it |
| 358 | return None |
| 359 | |
| 360 | def visit_func_def(self, defn: FuncDef) -> None: |
| 361 | start_line = defn.line - 1 |
| 362 | start_indent = None |
| 363 | # When a function is decorated, sometimes the start line will point to |
| 364 | # whitespace or comments between the decorator and the function, so |
| 365 | # we have to look for the start. |
| 366 | while start_line < len(self.source): |
| 367 | start_indent = self.indentation_level(start_line) |
| 368 | if start_indent is not None: |
| 369 | break |
| 370 | start_line += 1 |
| 371 | # If we can't find the function give up and don't annotate anything. |
| 372 | # Our line numbers are not reliable enough to be asserting on. |
no outgoing calls
no test coverage detected
searching dependent graphs…