Coverage for coverage / parser.py: 100.000%
576 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
4"""Code parsing for coverage.py."""
6from __future__ import annotations 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
8import ast 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
9import collections 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
10import functools 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
11import os 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
12import re 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
13import token 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
14import tokenize 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
15from collections.abc import Iterable, Sequence 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
16from dataclasses import dataclass 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
17from types import CodeType 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
18from typing import Callable, Optional, Protocol, cast 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
20from coverage import env 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
21from coverage.bytecode import code_objects 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
22from coverage.debug import short_stack 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
23from coverage.exceptions import NoSource, NotPython 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
24from coverage.misc import isolate_module, nice_pair 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
25from coverage.phystokens import generate_tokens 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
26from coverage.types import TArc, TLineNo 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
28os = isolate_module(os) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
31class PythonParser: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
32 """Parse code to find executable lines, excluded lines, etc.
34 This information is all based on static analysis: no code execution is
35 involved.
37 """
39 def __init__( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234
40 self,
41 text: str | None = None,
42 filename: str | None = None,
43 exclude: str | None = None,
44 ) -> None:
45 """
46 Source can be provided as `text`, the text itself, or `filename`, from
47 which the text will be read. Excluded lines are those that match
48 `exclude`, a regex string.
50 """
51 assert text or filename, "PythonParser needs either text or filename" 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
52 self.filename = filename or "<code>" 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
53 if text is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
54 self.text: str = text 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
55 else:
56 from coverage.python import get_python_source 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
58 try: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
59 self.text = get_python_source(self.filename) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
60 except OSError as err: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
61 raise NoSource(f"No source for code: '{self.filename}': {err}") from err 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
63 self.exclude = exclude 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
65 # The parsed AST of the text.
66 self._ast_root: ast.AST | None = None 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
68 # The normalized line numbers of the statements in the code. Exclusions
69 # are taken into account, and statements are adjusted to their first
70 # lines.
71 self.statements: set[TLineNo] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
73 # The normalized line numbers of the excluded lines in the code,
74 # adjusted to their first lines.
75 self.excluded: set[TLineNo] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
77 # The raw_* attributes are only used in this class, and in
78 # lab/parser.py to show how this class is working.
80 # The line numbers that start statements, as reported by the line
81 # number table in the bytecode.
82 self.raw_statements: set[TLineNo] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
84 # The raw line numbers of excluded lines of code, as marked by pragmas.
85 self.raw_excluded: set[TLineNo] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
87 # The line numbers of docstring lines.
88 self.raw_docstrings: set[TLineNo] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
90 # Internal detail, used by lab/parser.py.
91 self.show_tokens = False 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
93 # A dict mapping line numbers to lexical statement starts for
94 # multi-line statements.
95 self.multiline_map: dict[TLineNo, TLineNo] = {} 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
97 # Lazily-created arc data, and missing arc descriptions.
98 self._all_arcs: set[TArc] | None = None 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
99 self._missing_arc_fragments: TArcFragments | None = None 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
100 self._with_jump_fixers: dict[TArc, tuple[TArc, TArc]] = {} 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
102 def lines_matching(self, regex: str) -> set[TLineNo]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
103 """Find the lines matching a regex.
105 Returns a set of line numbers, the lines that contain a match for
106 `regex`. The entire line needn't match, just a part of it.
107 Handles multiline regex patterns.
109 """
110 matches: set[TLineNo] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
112 last_start = 0 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
113 last_start_line = 0 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
114 for match in re.finditer(regex, self.text, flags=re.MULTILINE): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
115 start, end = match.span() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
116 start_line = last_start_line + self.text.count("\n", last_start, start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
117 end_line = last_start_line + self.text.count("\n", last_start, end) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
118 matches.update( 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
119 self.multiline_map.get(i, i) for i in range(start_line + 1, end_line + 2)
120 )
121 last_start = start 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
122 last_start_line = start_line 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
123 return matches 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
125 def _raw_parse(self) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
126 """Parse the source to find the interesting facts about its lines.
128 A handful of attributes are updated.
130 """
131 # Find lines which match an exclusion pattern.
132 if self.exclude: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
133 self.raw_excluded = self.lines_matching(self.exclude) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
134 self.excluded = set(self.raw_excluded) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
136 # The current number of indents.
137 indent: int = 0 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
138 # An exclusion comment will exclude an entire clause at this indent.
139 exclude_indent: int = 0 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
140 # Are we currently excluding lines?
141 excluding: bool = False 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
142 # The line number of the first line in a multi-line statement.
143 first_line: int = 0 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
144 # Is the file empty?
145 empty: bool = True 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
146 # Parenthesis (and bracket) nesting level.
147 nesting: int = 0 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
149 assert self.text is not None 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
150 tokgen = generate_tokens(self.text) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
151 for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
152 if self.show_tokens: # pragma: debugging 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
153 print(
154 "%10s %5s %-20r %r"
155 % (
156 tokenize.tok_name.get(toktype, toktype),
157 nice_pair((slineno, elineno)),
158 ttext,
159 ltext,
160 )
161 )
162 if toktype == token.INDENT: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
163 indent += 1 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
164 elif toktype == token.DEDENT: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
165 indent -= 1 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
166 elif toktype == token.OP: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
167 if ttext == ":" and nesting == 0: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
168 should_exclude = self.excluded.intersection(range(first_line, elineno + 1)) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
169 if not excluding and should_exclude: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
170 # Start excluding a suite. We trigger off of the colon
171 # token so that the #pragma comment will be recognized on
172 # the same line as the colon.
173 self.excluded.add(elineno) 1stuvwxyzABCDabcdefghijklmnopqrHIJK7LM%NOPQ8RS!TUVW$XY'Z012634#EFG
174 exclude_indent = indent 1stuvwxyzABCDabcdefghijklmnopqrHIJK7LM%NOPQ8RS!TUVW$XY'Z012634#EFG
175 excluding = True 1stuvwxyzABCDabcdefghijklmnopqrHIJK7LM%NOPQ8RS!TUVW$XY'Z012634#EFG
176 elif ttext in "([{": 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
177 nesting += 1 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
178 elif ttext in ")]}": 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
179 nesting -= 1 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
180 elif toktype == token.NEWLINE: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
181 if first_line and elineno != first_line: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
182 # We're at the end of a line, and we've ended on a
183 # different line than the first line of the statement,
184 # so record a multi-line range.
185 for l in range(first_line, elineno + 1): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
186 self.multiline_map[l] = first_line 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
187 first_line = 0 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
189 if ttext.strip() and toktype != tokenize.COMMENT: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
190 # A non-white-space token.
191 empty = False 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
192 if not first_line: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
193 # The token is not white space, and is the first in a statement.
194 first_line = slineno 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
195 # Check whether to end an excluded suite.
196 if excluding and indent <= exclude_indent: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
197 excluding = False 1stuvwxyzABCDabcdefghijklmnopqrHIJK7LM%NOPQ8RS!TUVW$XY'Z012634#EFG
198 if excluding: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
199 self.excluded.add(elineno) 1stuvwxyzABCDabcdefghijklmnopqrHIJK7LM%NOPQ8RS!TUVW$XY'Z012634#EFG
201 # Find the starts of the executable statements.
202 if not empty: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
203 byte_parser = ByteParser(self.text, filename=self.filename) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
204 self.raw_statements.update(byte_parser._find_statements()) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
206 self.excluded = self.first_lines(self.excluded) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
208 # AST lets us find classes, docstrings, and decorator-affected
209 # functions and classes.
210 assert self._ast_root is not None 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
211 for node in ast.walk(self._ast_root): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
212 # Find docstrings.
213 if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef, ast.Module)): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
214 if node.body: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
215 first = node.body[0] 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
216 if ( 1stuvwxyzABCD)7%98!($'56#EFG
217 isinstance(first, ast.Expr)
218 and isinstance(first.value, ast.Constant)
219 and isinstance(first.value.value, str)
220 ):
221 self.raw_docstrings.update( 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
222 range(first.lineno, cast(int, first.end_lineno) + 1)
223 )
224 # Exclusions carry from decorators and signatures to the bodies of
225 # functions and classes.
226 if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
227 first_line = min((d.lineno for d in node.decorator_list), default=node.lineno) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
228 if self.excluded.intersection(range(first_line, node.lineno + 1)): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
229 self.excluded.update(range(first_line, cast(int, node.end_lineno) + 1)) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
231 @functools.lru_cache(maxsize=1000) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
232 def first_line(self, lineno: TLineNo) -> TLineNo: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
233 """Return the first line number of the statement including `lineno`."""
234 if lineno < 0: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
235 lineno = -self.multiline_map.get(-lineno, -lineno) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
236 else:
237 lineno = self.multiline_map.get(lineno, lineno) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
238 return lineno 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
240 def first_lines(self, linenos: Iterable[TLineNo]) -> set[TLineNo]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
241 """Map the line numbers in `linenos` to the correct first line of the
242 statement.
244 Returns a set of the first lines.
246 """
247 return {self.first_line(l) for l in linenos} 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
249 def translate_lines(self, lines: Iterable[TLineNo]) -> set[TLineNo]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
250 """Implement `FileReporter.translate_lines`."""
251 return self.first_lines(lines) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
253 def translate_arcs(self, arcs: Iterable[TArc]) -> set[TArc]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
254 """Implement `FileReporter.translate_arcs`."""
255 return {(self.first_line(a), self.first_line(b)) for (a, b) in self.fix_with_jumps(arcs)} 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
257 def parse_source(self) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
258 """Parse source text to find executable lines, excluded lines, etc.
260 Sets the .excluded and .statements attributes, normalized to the first
261 line of multi-line statements.
263 """
264 try: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
265 self._ast_root = ast.parse(self.text) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
266 self._raw_parse() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
267 except (tokenize.TokenError, IndentationError, SyntaxError) as err: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
268 if hasattr(err, "lineno"): 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
269 lineno = err.lineno # IndentationError 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
270 else:
271 lineno = err.args[1][0] # TokenError 1stuvwxyzABCDEFG
272 raise NotPython( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
273 f"Couldn't parse '{self.filename}' as Python source: "
274 + f"{err.args[0]!r} at line {lineno}",
275 ) from err
277 ignore = self.excluded | self.raw_docstrings 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
278 starts = self.raw_statements - ignore 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
279 self.statements = self.first_lines(starts) - ignore 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
281 def arcs(self) -> set[TArc]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
282 """Get information about the arcs available in the code.
284 Returns a set of line number pairs. Line numbers have been normalized
285 to the first line of multi-line statements.
287 """
288 if self._all_arcs is None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
289 self._analyze_ast() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
290 assert self._all_arcs is not None 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
291 return self._all_arcs 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
293 def _analyze_ast(self) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
294 """Run the AstArcAnalyzer and save its results.
296 `_all_arcs` is the set of arcs in the code.
298 """
299 assert self._ast_root is not None 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
300 aaa = AstArcAnalyzer(self.filename, self._ast_root, self.raw_statements, self.multiline_map) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
301 aaa.analyze() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
302 arcs = aaa.arcs 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
303 self._with_jump_fixers = aaa.with_jump_fixers() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
304 if self._with_jump_fixers: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
305 arcs = self.fix_with_jumps(arcs) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
307 self._all_arcs = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
308 for l1, l2 in arcs: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
309 fl1 = self.first_line(l1) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
310 fl2 = self.first_line(l2) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
311 if fl1 != fl2: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
312 self._all_arcs.add((fl1, fl2)) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
314 self._missing_arc_fragments = aaa.missing_arc_fragments 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
316 def fix_with_jumps(self, arcs: Iterable[TArc]) -> set[TArc]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
317 """Adjust arcs to fix jumps leaving `with` statements.
319 Consider this code:
321 with open("/tmp/test", "w") as f1:
322 a = 2
323 b = 3
324 print(4)
326 In 3.10+, we get traces for lines 1, 2, 3, 1, 4. But we want to present
327 it to the user as if it had been 1, 2, 3, 4. The arc 3->1 should be
328 replaced with 3->4, and 1->4 should be removed.
330 For this code, the fixers dict is {(3, 1): ((1, 4), (3, 4))}. The key
331 is the actual measured arc from the end of the with block back to the
332 start of the with-statement. The values are start_next (the with
333 statement to the next statement after the with), and end_next (the end
334 of the with-statement to the next statement after the with).
336 With nested with-statements, we have to trace through a few levels to
337 correct a longer chain of arcs.
339 """
340 to_remove = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
341 to_add = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
342 for arc in arcs: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
343 if arc in self._with_jump_fixers: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
344 end0 = arc[0] 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
345 to_remove.add(arc) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
346 start_next, end_next = self._with_jump_fixers[arc] 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
347 while start_next in self._with_jump_fixers: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
348 to_remove.add(start_next) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
349 start_next, end_next = self._with_jump_fixers[start_next] 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
350 to_remove.add(end_next) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
351 to_add.add((end0, end_next[1])) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
352 to_remove.add(start_next) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
353 arcs = (set(arcs) | to_add) - to_remove 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
354 return arcs 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
356 @functools.lru_cache 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
357 def exit_counts(self) -> dict[TLineNo, int]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
358 """Get a count of exits from that each line.
360 Excluded lines are excluded.
362 """
363 exit_counts: dict[TLineNo, int] = collections.defaultdict(int) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
364 for l1, l2 in self.arcs(): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
365 assert l1 > 0, f"{l1=} should be greater than zero in {self.filename}" 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
366 if l1 in self.excluded: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
367 # Don't report excluded lines as line numbers.
368 continue 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
369 if l2 in self.excluded: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
370 # Arcs to excluded lines shouldn't count.
371 continue 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
372 exit_counts[l1] += 1 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
374 return exit_counts 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
376 def _finish_action_msg(self, action_msg: str | None, end: TLineNo) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
377 """Apply some defaulting and formatting to an arc's description."""
378 if action_msg is None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
379 if end < 0: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
380 action_msg = "jump to the function exit" 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
381 else:
382 action_msg = "jump to line {lineno}" 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LMNO9PQ8RS!TU(VW$XYZ0512634EFG
383 action_msg = action_msg.format(lineno=end) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
384 return action_msg 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
386 def missing_arc_description(self, start: TLineNo, end: TLineNo) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
387 """Provide an English sentence describing a missing arc."""
388 if self._missing_arc_fragments is None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
389 self._analyze_ast() 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNO9PQRSTUVWXYZ051234EFG
390 assert self._missing_arc_fragments is not None 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNO9PQRSTUVWXYZ051234EFG
392 fragment_pairs = self._missing_arc_fragments.get((start, end), [(None, None)]) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
394 msgs = [] 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
395 for missing_cause_msg, action_msg in fragment_pairs: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
396 action_msg = self._finish_action_msg(action_msg, end) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
397 msg = f"line {start} didn't {action_msg}" 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
398 if missing_cause_msg is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
399 msg += f" because {missing_cause_msg.format(lineno=start)}" 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LMNO9PQRS!TU(VW$XYZ0512634#EFG
401 msgs.append(msg) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
403 return " or ".join(msgs) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
405 def arc_description(self, start: TLineNo, end: TLineNo) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
406 """Provide an English description of an arc's effect."""
407 if self._missing_arc_fragments is None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
408 self._analyze_ast() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
409 assert self._missing_arc_fragments is not None 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
411 fragment_pairs = self._missing_arc_fragments.get((start, end), [(None, None)]) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
412 action_msg = self._finish_action_msg(fragment_pairs[0][1], end) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
413 return action_msg 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
416class ByteParser: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
417 """Parse bytecode to understand the structure of code."""
419 def __init__( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234
420 self,
421 text: str,
422 code: CodeType | None = None,
423 filename: str | None = None,
424 ) -> None:
425 self.text = text 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
426 if code is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
427 self.code = code 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
428 else:
429 assert filename is not None 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
430 # We only get here if earlier ast parsing succeeded, so no need to
431 # catch errors.
432 self.code = compile(text, filename, "exec", dont_inherit=True) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
434 def child_parsers(self) -> Iterable[ByteParser]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
435 """Iterate over all the code objects nested within this one.
437 The iteration includes `self` as its first value.
439 We skip code objects named `__annotate__` since they are deferred
440 annotations that usually are never run. If there are errors in the
441 annotations, they will be caught by type checkers or other tools that
442 use annotations.
444 """
445 return ( 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
446 ByteParser(self.text, code=c)
447 for c in code_objects(self.code)
448 if c.co_name != "__annotate__"
449 )
451 def _line_numbers(self) -> Iterable[TLineNo]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
452 """Yield the line numbers possible in this code object.
454 Uses co_lines() to produce a sequence: l0, l1, ...
455 """
456 for _, _, line in self.code.co_lines(): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
457 if line: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
458 yield line 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
460 def _find_statements(self) -> Iterable[TLineNo]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
461 """Find the statements in `self.code`.
463 Produce a sequence of line numbers that start statements. Recurses
464 into all code objects reachable from `self.code`.
466 """
467 for bp in self.child_parsers(): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
468 # Get all of the lineno information from this code.
469 yield from bp._line_numbers() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
472#
473# AST analysis
474#
477@dataclass(frozen=True, order=True) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
478class ArcStart: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
479 """The information needed to start an arc.
481 `lineno` is the line number the arc starts from.
483 `cause` is an English text fragment used as the `missing_cause_msg` for
484 AstArcAnalyzer.missing_arc_fragments. It will be used to describe why an
485 arc wasn't executed, so should fit well into a sentence of the form,
486 "Line 17 didn't run because {cause}." The fragment can include "{lineno}"
487 to have `lineno` interpolated into it.
489 As an example, this code::
491 if something(x): # line 1
492 func(x) # line 2
493 more_stuff() # line 3
495 would have two ArcStarts:
497 - ArcStart(1, "the condition on line 1 was always true")
498 - ArcStart(1, "the condition on line 1 was never true")
500 The first would be used to create an arc from 1 to 3, creating a message like
501 "line 1 didn't jump to line 3 because the condition on line 1 was always true."
503 The second would be used for the arc from 1 to 2, creating a message like
504 "line 1 didn't jump to line 2 because the condition on line 1 was never true."
506 """
508 lineno: TLineNo 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
509 cause: str = "" 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
512class TAddArcFn(Protocol): 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
513 """The type for AstArcAnalyzer.add_arc().""" 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
515 def __call__( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234
516 self,
517 start: TLineNo, 1abcdefghijklmnopqrEFG
518 end: TLineNo, 1abcdefghijklmnopqrEFG
519 missing_cause_msg: str | None = None, 1stuvwxyzABCDabcdefghijklmnopqrEFG
520 action_msg: str | None = None, 1stuvwxyzABCDabcdefghijklmnopqrEFG
521 ) -> None: 1abcdefghijklmnopqrEFG
522 """
523 Record an arc from `start` to `end`.
525 `missing_cause_msg` is a description of the reason the arc wasn't
526 taken if it wasn't taken. For example, "the condition on line 10 was
527 never true."
529 `action_msg` is a description of what the arc does, like "jump to line
530 10" or "exit from function 'fooey'."
532 """
535TArcFragments = dict[TArc, list[tuple[Optional[str], Optional[str]]]] 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
538class Block: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
539 """
540 Blocks need to handle various exiting statements in their own ways.
542 All of these methods take a list of exits, and a callable `add_arc`
543 function that they can use to add arcs if needed. They return True if the
544 exits are handled, or False if the search should continue up the block
545 stack.
546 """
548 # pylint: disable=unused-argument
549 def process_break_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
550 """Process break exits."""
551 return False 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
553 def process_continue_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
554 """Process continue exits."""
555 return False 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
557 def process_raise_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
558 """Process raise exits."""
559 return False 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
561 def process_return_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
562 """Process return exits."""
563 return False 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
566class LoopBlock(Block): 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
567 """A block on the block stack representing a `for` or `while` loop."""
569 def __init__(self, start: TLineNo) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
570 # The line number where the loop starts.
571 self.start = start 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
572 # A set of ArcStarts, the arcs from break statements exiting this loop.
573 self.break_exits: set[ArcStart] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
575 def process_break_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
576 self.break_exits.update(exits) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
577 return True 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
579 def process_continue_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
580 for xit in exits: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
581 add_arc(xit.lineno, self.start, xit.cause) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
582 return True 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
585class FunctionBlock(Block): 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
586 """A block on the block stack representing a function definition."""
588 def __init__(self, start: TLineNo, name: str) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
589 # The line number where the function starts.
590 self.start = start 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
591 # The name of the function.
592 self.name = name 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
594 def process_raise_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
595 for xit in exits: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
596 add_arc( 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
597 xit.lineno,
598 -self.start,
599 xit.cause,
600 f"except from function {self.name!r}",
601 )
602 return True 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
604 def process_return_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
605 for xit in exits: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
606 add_arc( 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
607 xit.lineno,
608 -self.start,
609 xit.cause,
610 f"return from function {self.name!r}",
611 )
612 return True 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
615class TryBlock(Block): 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
616 """A block on the block stack representing a `try` block."""
618 def __init__(self, handler_start: TLineNo | None, final_start: TLineNo | None) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
619 # The line number of the first "except" handler, if any.
620 self.handler_start = handler_start 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
621 # The line number of the "finally:" clause, if any.
622 self.final_start = final_start 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
624 def process_raise_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
625 if self.handler_start is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
626 for xit in exits: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
627 add_arc(xit.lineno, self.handler_start, xit.cause) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
628 return True 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
631# TODO: Shouldn't the cause messages join with "and" instead of "or"?
634def is_constant_test_expr(node: ast.AST) -> tuple[bool, bool]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
635 """Is this a compile-time constant test expression?
637 We don't try to mimic all of CPython's optimizations. We just have to
638 handle the kinds of constant expressions people might actually use.
640 """
641 match node: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
642 case ast.Constant(): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
643 return True, bool(node.value) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNO9PQRSTUVWXYZ01234EFG
644 case ast.Name(): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
645 if node.id in ["True", "False", "None", "__debug__"]: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
646 return True, eval(node.id) # pylint: disable=eval-used 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
647 case ast.UnaryOp(): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
648 if isinstance(node.op, ast.Not): 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQ8RSTU(VWXYZ012634EFG
649 is_constant, val = is_constant_test_expr(node.operand) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQ8RSTU(VWXYZ012634EFG
650 return is_constant, not val 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQ8RSTU(VWXYZ012634EFG
651 case ast.BoolOp(): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
652 rets = [is_constant_test_expr(v) for v in node.values] 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNO9PQRSTUVWXYZ01234EFG
653 is_constant = all(is_const for is_const, _ in rets) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
654 if is_constant: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNO9PQRSTUVWXYZ01234EFG
655 op = any if isinstance(node.op, ast.Or) else all 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
656 return True, op(v for _, v in rets) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
657 return False, False 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
660class AstArcAnalyzer: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
661 """Analyze source text with an AST to find executable code paths.
663 The .analyze() method does the work, and populates these attributes:
665 `arcs`: a set of (from, to) pairs of the the arcs possible in the code.
667 `missing_arc_fragments`: a dict mapping (from, to) arcs to lists of
668 message fragments explaining why the arc is missing from execution::
670 { (start, end): [(missing_cause_msg, action_msg), ...], }
672 For an arc starting from line 17, they should be usable to form complete
673 sentences like: "Line 17 didn't {action_msg} because {missing_cause_msg}".
675 NOTE: Starting in July 2024, I've been whittling this down to only report
676 arc that are part of true branches. It's not clear how far this work will
677 go.
679 """
681 def __init__( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
682 self,
683 filename: str,
684 root_node: ast.AST,
685 statements: set[TLineNo],
686 multiline: dict[TLineNo, TLineNo],
687 ) -> None:
688 self.filename = filename 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
689 self.root_node = root_node 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
690 self.statements = {multiline.get(l, l) for l in statements} 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
691 self.multiline = multiline 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
693 # Turn on AST dumps with an environment variable.
694 # $set_env.py: COVERAGE_AST_DUMP - Dump the AST nodes when parsing code.
695 dump_ast = bool(int(os.getenv("COVERAGE_AST_DUMP", "0"))) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
697 if dump_ast: # pragma: debugging 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
698 # Dump the AST so that failing tests have helpful output.
699 print(f"Statements: {self.statements}")
700 print(f"Multiline map: {self.multiline}")
701 print(ast.dump(self.root_node, include_attributes=True, indent=4))
703 self.arcs: set[TArc] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
704 self.missing_arc_fragments: TArcFragments = collections.defaultdict(list) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
705 self.block_stack: list[Block] = [] 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
707 # If `with` clauses jump to their start on the way out, we need
708 # information to be able to skip over that jump. We record the arcs
709 # from `with` into the clause (with_entries), and the arcs from the
710 # clause to the `with` (with_exits).
711 self.current_with_starts: set[TLineNo] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
712 self.all_with_starts: set[TLineNo] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
713 self.with_entries: set[TArc] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
714 self.with_exits: set[TArc] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
716 # $set_env.py: COVERAGE_TRACK_ARCS - Trace possible arcs added while parsing code.
717 self.debug = bool(int(os.getenv("COVERAGE_TRACK_ARCS", "0"))) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
719 def analyze(self) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
720 """Examine the AST tree from `self.root_node` to determine possible arcs."""
721 for node in ast.walk(self.root_node): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
722 node_name = node.__class__.__name__ 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
723 code_object_handler = getattr(self, f"_code_object__{node_name}", None) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
724 if code_object_handler is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
725 code_object_handler(node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
727 def with_jump_fixers(self) -> dict[TArc, tuple[TArc, TArc]]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
728 """Get a dict with data for fixing jumps out of with statements.
730 Returns a dict. The keys are arcs leaving a with-statement by jumping
731 back to its start. The values are pairs: first, the arc from the start
732 to the next statement, then the arc that exits the with without going
733 to the start.
735 """
736 fixers = {} 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
737 with_nexts = { 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
738 arc
739 for arc in self.arcs
740 if arc[0] in self.all_with_starts and arc not in self.with_entries
741 }
742 for start in self.all_with_starts: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
743 nexts = {arc[1] for arc in with_nexts if arc[0] == start} 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
744 if not nexts: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
745 continue 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
746 assert len(nexts) == 1, f"Expected one arc, got {nexts} with {start = }" 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
747 nxt = nexts.pop() 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
748 ends = {arc[0] for arc in self.with_exits if arc[1] == start} 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
749 for end in ends: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
750 fixers[(end, start)] = ((start, nxt), (end, nxt)) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
751 return fixers 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
753 # Code object dispatchers: _code_object__*
754 #
755 # These methods are used by analyze() as the start of the analysis.
756 # There is one for each construct with a code object.
758 def _code_object__Module(self, node: ast.Module) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
759 start = self.line_for_node(node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
760 if node.body: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
761 exits = self.process_body(node.body) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
762 for xit in exits: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
763 self.add_arc(xit.lineno, -start, xit.cause, "exit the module") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
764 else:
765 # Empty module.
766 self.add_arc(start, -start) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVW$XYZ01234EFG
768 def _code_object__FunctionDef(self, node: ast.FunctionDef) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
769 start = self.line_for_node(node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
770 self.block_stack.append(FunctionBlock(start=start, name=node.name)) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
771 exits = self.process_body(node.body) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
772 self.process_return_exits(exits) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
773 self.block_stack.pop() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
775 _code_object__AsyncFunctionDef = _code_object__FunctionDef 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
777 def _code_object__ClassDef(self, node: ast.ClassDef) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
778 start = self.line_for_node(node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
779 exits = self.process_body(node.body) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
780 for xit in exits: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
781 self.add_arc(xit.lineno, -start, xit.cause, f"exit class {node.name!r}") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
783 def add_arc( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234
784 self,
785 start: TLineNo,
786 end: TLineNo,
787 missing_cause_msg: str | None = None,
788 action_msg: str | None = None,
789 ) -> None:
790 """Add an arc, including message fragments to use if it is missing."""
791 if self.debug: # pragma: debugging 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
792 print(f"Adding possible arc: ({start}, {end}): {missing_cause_msg!r}, {action_msg!r}")
793 print(short_stack(), end="\n\n")
794 self.arcs.add((start, end)) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
795 if start in self.current_with_starts: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
796 self.with_entries.add((start, end)) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
798 if missing_cause_msg is not None or action_msg is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
799 self.missing_arc_fragments[(start, end)].append((missing_cause_msg, action_msg)) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
801 def nearest_blocks(self) -> Iterable[Block]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
802 """Yield the blocks in nearest-to-farthest order."""
803 return reversed(self.block_stack) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
805 def line_for_node(self, node: ast.AST) -> TLineNo: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
806 """What is the right line number to use for this node?
808 This dispatches to _line__Node functions where needed.
810 """
811 node_name = node.__class__.__name__ 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
812 handler = cast( 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
813 Optional[Callable[[ast.AST], TLineNo]],
814 getattr(self, f"_line__{node_name}", None),
815 )
816 if handler is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
817 line = handler(node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
818 else:
819 line = node.lineno # type: ignore[attr-defined] 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
820 return self.multiline.get(line, line) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
822 # First lines: _line__*
823 #
824 # Dispatched by line_for_node, each method knows how to identify the first
825 # line number in the node, as Python will report it.
827 def _line_decorated(self, node: ast.FunctionDef) -> TLineNo: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
828 """Compute first line number for things that can be decorated (classes and functions)."""
829 if node.decorator_list: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
830 lineno = node.decorator_list[0].lineno 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
831 else:
832 lineno = node.lineno 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
833 return lineno 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
835 def _line__Assign(self, node: ast.Assign) -> TLineNo: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
836 return self.line_for_node(node.value) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
838 _line__ClassDef = _line_decorated 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
840 def _line__Dict(self, node: ast.Dict) -> TLineNo: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
841 if node.keys: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
842 if node.keys[0] is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
843 return node.keys[0].lineno 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
844 else:
845 # Unpacked dict literals `{**{"a":1}}` have None as the key,
846 # use the value in that case.
847 return node.values[0].lineno 1stuvwxyzABCDabcdefghijklmnopqrHIJKLM%NOPQ8RS!TUVW$XY'Z012634#EFG
848 else:
849 return node.lineno 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234#EFG
851 _line__FunctionDef = _line_decorated 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
852 _line__AsyncFunctionDef = _line_decorated 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
854 def _line__List(self, node: ast.List) -> TLineNo: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
855 if node.elts: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
856 return self.line_for_node(node.elts[0]) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
857 else:
858 return node.lineno 1stuvwxyzABCDabcdefghijklmnopqrHIJK7LMNOPQRSTUVWXYZ051234EFG
860 def _line__Module(self, node: ast.Module) -> TLineNo: # pylint: disable=unused-argument 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
861 return 1 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
863 # The node types that just flow to the next node with no complications.
864 OK_TO_DEFAULT = { 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
865 "AnnAssign",
866 "Assign",
867 "Assert",
868 "AugAssign",
869 "Delete",
870 "Expr",
871 "Global",
872 "Import",
873 "ImportFrom",
874 "Nonlocal",
875 "Pass",
876 }
878 def node_exits(self, node: ast.AST) -> set[ArcStart]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
879 """Find the set of arc starts that exit this node.
881 Return a set of ArcStarts, exits from this node to the next. Because a
882 node represents an entire sub-tree (including its children), the exits
883 from a node can be arbitrarily complex::
885 if something(1):
886 if other(2):
887 doit(3)
888 else:
889 doit(5)
891 There are three exits from line 1: they start at lines 1, 3 and 5.
892 There are two exits from line 2: lines 3 and 5.
894 """
895 node_name = node.__class__.__name__ 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
896 handler = cast( 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
897 Optional[Callable[[ast.AST], set[ArcStart]]],
898 getattr(self, f"_handle__{node_name}", None),
899 )
900 if handler is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
901 arc_starts = handler(node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
902 else:
903 # No handler: either it's something that's ok to default (a simple
904 # statement), or it's something we overlooked.
905 if env.TESTING: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
906 if node_name not in self.OK_TO_DEFAULT: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
907 raise RuntimeError(f"*** Unhandled: {node}") # pragma: only failure
909 # Default for simple statements: one exit from this node.
910 arc_starts = {ArcStart(self.line_for_node(node))} 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
911 return arc_starts 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
913 def process_body( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234
914 self,
915 body: Sequence[ast.AST],
916 from_start: ArcStart | None = None,
917 prev_starts: set[ArcStart] | None = None,
918 ) -> set[ArcStart]:
919 """Process the body of a compound statement.
921 `body` is the body node to process.
923 `from_start` is a single `ArcStart` that starts an arc into this body.
924 `prev_starts` is a set of ArcStarts that can all be the start of arcs
925 into this body. Only one of `from_start` and `prev_starts` should be
926 given.
928 Records arcs within the body by calling `self.add_arc`.
930 Returns a set of ArcStarts, the exits from this body.
932 """
933 if prev_starts is None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
934 if from_start is None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
935 prev_starts = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
936 else:
937 prev_starts = {from_start} 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
938 else:
939 assert from_start is None 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
941 # Loop over the nodes in the body, making arcs from each one's exits to
942 # the next node.
943 for body_node in body: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
944 lineno = self.line_for_node(body_node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
945 if lineno not in self.statements: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
946 continue 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTU(VWXYZ01234EFG
947 for prev_start in prev_starts: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
948 self.add_arc(prev_start.lineno, lineno, prev_start.cause) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
949 prev_starts = self.node_exits(body_node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
950 return prev_starts 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
952 # Exit processing: process_*_exits
953 #
954 # These functions process the four kinds of jump exits: break, continue,
955 # raise, and return. To figure out where an exit goes, we have to look at
956 # the block stack context. For example, a break will jump to the nearest
957 # enclosing loop block, or the nearest enclosing finally block, whichever
958 # is nearer.
960 def process_break_exits(self, exits: set[ArcStart]) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
961 """Add arcs due to jumps from `exits` being breaks."""
962 for block in self.nearest_blocks(): # pragma: always breaks 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
963 if block.process_break_exits(exits, self.add_arc): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
964 break 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
966 def process_continue_exits(self, exits: set[ArcStart]) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
967 """Add arcs due to jumps from `exits` being continues."""
968 for block in self.nearest_blocks(): # pragma: always breaks 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
969 if block.process_continue_exits(exits, self.add_arc): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
970 break 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
972 def process_raise_exits(self, exits: set[ArcStart]) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
973 """Add arcs due to jumps from `exits` being raises."""
974 for block in self.nearest_blocks(): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
975 if block.process_raise_exits(exits, self.add_arc): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
976 break 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
978 def process_return_exits(self, exits: set[ArcStart]) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
979 """Add arcs due to jumps from `exits` being returns."""
980 for block in self.nearest_blocks(): # pragma: always breaks 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
981 if block.process_return_exits(exits, self.add_arc): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
982 break 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
984 # Node handlers: _handle__*
985 #
986 # Each handler deals with a specific AST node type, dispatched from
987 # node_exits. Handlers return the set of exits from that node, and can
988 # also call self.add_arc to record arcs they find. These functions mirror
989 # the Python semantics of each syntactic construct. See the docstring
990 # for node_exits to understand the concept of exits from a node.
991 #
992 # Every node type that represents a statement should have a handler, or it
993 # should be listed in OK_TO_DEFAULT.
995 def _handle__Break(self, node: ast.Break) -> set[ArcStart]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
996 here = self.line_for_node(node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
997 break_start = ArcStart(here, cause="the break on line {lineno} wasn't executed") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
998 self.process_break_exits({break_start}) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
999 return set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1001 def _handle_decorated(self, node: ast.FunctionDef) -> set[ArcStart]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1002 """Add arcs for things that can be decorated (classes and functions)."""
1003 main_line: TLineNo = node.lineno 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1004 last: TLineNo | None = node.lineno 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1005 decs = node.decorator_list 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1006 if decs: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1007 last = None 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1008 for dec_node in decs: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1009 dec_start = self.line_for_node(dec_node) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1010 if last is not None and dec_start != last: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1011 self.add_arc(last, dec_start) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1012 last = dec_start 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1013 assert last is not None 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1014 self.add_arc(last, main_line) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1015 last = main_line 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1016 # The definition line may have been missed, but we should have it
1017 # in `self.statements`. For some constructs, `line_for_node` is
1018 # not what we'd think of as the first line in the statement, so map
1019 # it to the first one.
1020 assert node.body, f"Oops: {node.body = } in {self.filename}@{node.lineno}" 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1021 # The body is handled in collect_arcs.
1022 assert last is not None 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1023 return {ArcStart(last)} 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1025 _handle__ClassDef = _handle_decorated 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1027 def _handle__Continue(self, node: ast.Continue) -> set[ArcStart]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1028 here = self.line_for_node(node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1029 continue_start = ArcStart(here, cause="the continue on line {lineno} wasn't executed") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1030 self.process_continue_exits({continue_start}) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1031 return set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1033 def _handle__For(self, node: ast.For) -> set[ArcStart]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1034 start = self.line_for_node(node.iter) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1035 self.block_stack.append(LoopBlock(start=start)) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1036 from_start = ArcStart(start, cause="the loop on line {lineno} never started") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1037 exits = self.process_body(node.body, from_start=from_start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1038 # Any exit from the body will go back to the top of the loop.
1039 for xit in exits: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1040 self.add_arc(xit.lineno, start, xit.cause) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1041 my_block = self.block_stack.pop() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1042 assert isinstance(my_block, LoopBlock) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1043 exits = my_block.break_exits 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1044 from_start = ArcStart(start, cause="the loop on line {lineno} didn't complete") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1045 if node.orelse: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1046 else_exits = self.process_body(node.orelse, from_start=from_start) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1047 exits |= else_exits 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1048 else:
1049 # No else clause: exit from the for line.
1050 exits.add(from_start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1051 return exits 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1053 _handle__AsyncFor = _handle__For 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1055 _handle__FunctionDef = _handle_decorated 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1056 _handle__AsyncFunctionDef = _handle_decorated 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1058 def _handle__If(self, node: ast.If) -> set[ArcStart]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1059 start = self.line_for_node(node.test) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1060 constant_test, val = is_constant_test_expr(node.test) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1061 exits = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1062 if not constant_test or val: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1063 from_start = ArcStart(start, cause="the condition on line {lineno} was never true") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1064 exits |= self.process_body(node.body, from_start=from_start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1065 if not constant_test or not val: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1066 from_start = ArcStart(start, cause="the condition on line {lineno} was always true") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1067 exits |= self.process_body(node.orelse, from_start=from_start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1068 return exits 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1070 def _handle__Match(self, node: ast.Match) -> set[ArcStart]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1071 start = self.line_for_node(node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1072 last_start = start 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1073 exits = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1074 for case in node.cases: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1075 case_start = self.line_for_node(case.pattern) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1076 self.add_arc(last_start, case_start, "the pattern on line {lineno} always matched") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1077 from_start = ArcStart( 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1078 case_start,
1079 cause="the pattern on line {lineno} never matched",
1080 )
1081 exits |= self.process_body(case.body, from_start=from_start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1082 last_start = case_start 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1084 # case is now the last case, check for wildcard match.
1085 pattern = case.pattern # pylint: disable=undefined-loop-variable 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1086 while isinstance(pattern, ast.MatchOr): 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1087 pattern = pattern.patterns[-1] 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRS!TUVWXYZ01234EFG
1088 while isinstance(pattern, ast.MatchAs) and pattern.pattern is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1089 pattern = pattern.pattern 1stuvwxyzABCDabcdefghijklmnopqrHIJK7LM%NOPQ8RS!TUVWXY'Z012634#EFG
1090 had_wildcard = ( 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1091 isinstance(pattern, ast.MatchAs) and pattern.pattern is None and case.guard is None # pylint: disable=undefined-loop-variable
1092 )
1094 if not had_wildcard: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1095 exits.add( 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQRSTUVWXY'Z01234#EFG
1096 ArcStart(case_start, cause="the pattern on line {lineno} always matched"),
1097 )
1098 return exits 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1100 def _handle__Raise(self, node: ast.Raise) -> set[ArcStart]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1101 here = self.line_for_node(node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1102 raise_start = ArcStart(here, cause="the raise on line {lineno} wasn't executed") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1103 self.process_raise_exits({raise_start}) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1104 # `raise` statement jumps away, no exits from here.
1105 return set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1107 def _handle__Return(self, node: ast.Return) -> set[ArcStart]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1108 here = self.line_for_node(node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1109 return_start = ArcStart(here, cause="the return on line {lineno} wasn't executed") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1110 self.process_return_exits({return_start}) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1111 # `return` statement jumps away, no exits from here.
1112 return set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1114 def _handle__Try(self, node: ast.Try) -> set[ArcStart]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1115 if node.handlers: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1116 handler_start = self.line_for_node(node.handlers[0]) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1117 else:
1118 handler_start = None 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ051234EFG
1120 if node.finalbody: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1121 final_start = self.line_for_node(node.finalbody[0]) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ051234EFG
1122 else:
1123 final_start = None 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1125 # This is true by virtue of Python syntax: have to have either except
1126 # or finally, or both.
1127 assert handler_start is not None or final_start is not None 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1128 try_block = TryBlock(handler_start, final_start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1129 self.block_stack.append(try_block) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1131 start = self.line_for_node(node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1132 exits = self.process_body(node.body, from_start=ArcStart(start)) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1134 # We're done with the `try` body, so this block no longer handles
1135 # exceptions. We keep the block so the `finally` clause can pick up
1136 # flows from the handlers and `else` clause.
1137 if node.finalbody: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1138 try_block.handler_start = None 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ051234EFG
1139 else:
1140 self.block_stack.pop() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1142 handler_exits: set[ArcStart] = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1144 if node.handlers: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1145 for handler_node in node.handlers: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1146 handler_start = self.line_for_node(handler_node) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1147 from_cause = "the exception caught by line {lineno} didn't happen" 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1148 from_start = ArcStart(handler_start, cause=from_cause) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1149 handler_exits |= self.process_body(handler_node.body, from_start=from_start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1151 if node.orelse: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1152 exits = self.process_body(node.orelse, prev_starts=exits) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQ8RSTU(VWXYZ01234EFG
1154 exits |= handler_exits 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1156 if node.finalbody: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1157 self.block_stack.pop() 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ051234EFG
1158 final_from = exits 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ051234EFG
1160 final_exits = self.process_body(node.finalbody, prev_starts=final_from) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ051234EFG
1162 if exits: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ051234EFG
1163 # The finally clause's exits are only exits for the try block
1164 # as a whole if the try block had some exits to begin with.
1165 exits = final_exits 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ051234EFG
1167 return exits 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1169 _handle__TryStar = _handle__Try 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1171 def _handle__While(self, node: ast.While) -> set[ArcStart]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1172 start = to_top = self.line_for_node(node.test) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1173 constant_test, _ = is_constant_test_expr(node.test) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1174 self.block_stack.append(LoopBlock(start=to_top)) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1175 from_start = ArcStart(start, cause="the condition on line {lineno} was never true") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1176 exits = self.process_body(node.body, from_start=from_start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1177 for xit in exits: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1178 self.add_arc(xit.lineno, to_top, xit.cause) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1179 exits = set() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1180 my_block = self.block_stack.pop() 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1181 assert isinstance(my_block, LoopBlock) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1182 exits.update(my_block.break_exits) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1183 from_start = ArcStart(start, cause="the condition on line {lineno} was always true") 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1184 if node.orelse: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1185 else_exits = self.process_body(node.orelse, from_start=from_start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LMNO9PQRSTUVWXYZ0512634EFG
1186 exits |= else_exits 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LMNO9PQRSTUVWXYZ0512634EFG
1187 else:
1188 # No `else` clause: you can exit from the start.
1189 if not constant_test: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1190 exits.add(from_start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1191 return exits 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1193 def _handle__With(self, node: ast.With) -> set[ArcStart]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG
1194 if env.PYBEHAVIOR.exit_with_through_ctxmgr: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1195 starts = [self.line_for_node(item.context_expr) for item in node.items] 1abcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#
1196 else:
1197 starts = [self.line_for_node(node)] 1stuvwxyzABCDEFG
1198 for start in starts: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1199 self.current_with_starts.add(start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1200 self.all_with_starts.add(start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1202 exits = self.process_body(node.body, from_start=ArcStart(starts[-1])) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1204 start = starts[-1] 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1205 self.current_with_starts.remove(start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1206 with_exit = {ArcStart(start)} 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1207 if exits: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1208 for xit in exits: 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1209 self.add_arc(xit.lineno, start) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1210 self.with_exits.add((xit.lineno, start)) 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1211 exits = with_exit 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1213 return exits 1stuvwxyzABCDabcdefghijklmnopqrHI)JK7LM%NO9PQ8RS!TU(VW$XY'Z0512634#EFG
1215 _handle__AsyncWith = _handle__With 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG