DebugClients/Python3/coverage/parser.py

changeset 5051
3586ebd9fac8
parent 4489
d0d6e4ad31bd
equal deleted inserted replaced
5047:04e5dfbd3f3d 5051:3586ebd9fac8
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
3 3
4 """Code parsing for coverage.py.""" 4 """Code parsing for coverage.py."""
5 5
6 import ast
6 import collections 7 import collections
7 import dis 8 import os
8 import re 9 import re
9 import token 10 import token
10 import tokenize 11 import tokenize
11 12
13 from coverage import env
12 from coverage.backward import range # pylint: disable=redefined-builtin 14 from coverage.backward import range # pylint: disable=redefined-builtin
13 from coverage.backward import bytes_to_ints 15 from coverage.backward import bytes_to_ints, string_class
14 from coverage.bytecode import ByteCodes, CodeObjects 16 from coverage.bytecode import CodeObjects
15 from coverage.misc import contract, nice_pair, expensive, join_regex 17 from coverage.debug import short_stack
18 from coverage.misc import contract, new_contract, nice_pair, join_regex
16 from coverage.misc import CoverageException, NoSource, NotPython 19 from coverage.misc import CoverageException, NoSource, NotPython
17 from coverage.phystokens import compile_unicode, generate_tokens 20 from coverage.phystokens import compile_unicode, generate_tokens, neuter_encoding_declaration
18 21
19 22
20 class PythonParser(object): 23 class PythonParser(object):
21 """Parse code to find executable lines, excluded lines, etc.""" 24 """Parse code to find executable lines, excluded lines, etc.
22 25
26 This information is all based on static analysis: no code execution is
27 involved.
28
29 """
23 @contract(text='unicode|None') 30 @contract(text='unicode|None')
24 def __init__(self, text=None, filename=None, exclude=None): 31 def __init__(self, text=None, filename=None, exclude=None):
25 """ 32 """
26 Source can be provided as `text`, the text itself, or `filename`, from 33 Source can be provided as `text`, the text itself, or `filename`, from
27 which the text will be read. Excluded lines are those that match 34 which the text will be read. Excluded lines are those that match
40 "No source for code: '%s': %s" % (self.filename, err) 47 "No source for code: '%s': %s" % (self.filename, err)
41 ) 48 )
42 49
43 self.exclude = exclude 50 self.exclude = exclude
44 51
45 self.show_tokens = False
46
47 # The text lines of the parsed code. 52 # The text lines of the parsed code.
48 self.lines = self.text.split('\n') 53 self.lines = self.text.split('\n')
49 54
50 # The line numbers of excluded lines of code. 55 # The normalized line numbers of the statements in the code. Exclusions
56 # are taken into account, and statements are adjusted to their first
57 # lines.
58 self.statements = set()
59
60 # The normalized line numbers of the excluded lines in the code,
61 # adjusted to their first lines.
51 self.excluded = set() 62 self.excluded = set()
52 63
64 # The raw_* attributes are only used in this class, and in
65 # lab/parser.py to show how this class is working.
66
67 # The line numbers that start statements, as reported by the line
68 # number table in the bytecode.
69 self.raw_statements = set()
70
71 # The raw line numbers of excluded lines of code, as marked by pragmas.
72 self.raw_excluded = set()
73
74 # The line numbers of class and function definitions.
75 self.raw_classdefs = set()
76
53 # The line numbers of docstring lines. 77 # The line numbers of docstring lines.
54 self.docstrings = set() 78 self.raw_docstrings = set()
55 79
56 # The line numbers of class definitions. 80 # Internal detail, used by lab/parser.py.
57 self.classdefs = set() 81 self.show_tokens = False
58 82
59 # A dict mapping line numbers to (lo,hi) for multi-line statements. 83 # A dict mapping line numbers to lexical statement starts for
60 self.multiline = {} 84 # multi-line statements.
61 85 self._multiline = {}
62 # The line numbers that start statements. 86
63 self.statement_starts = set() 87 # Lazily-created ByteParser, arc data, and missing arc descriptions.
64
65 # Lazily-created ByteParser and arc data.
66 self._byte_parser = None 88 self._byte_parser = None
67 self._all_arcs = None 89 self._all_arcs = None
90 self._missing_arc_fragments = None
68 91
69 @property 92 @property
70 def byte_parser(self): 93 def byte_parser(self):
71 """Create a ByteParser on demand.""" 94 """Create a ByteParser on demand."""
72 if not self._byte_parser: 95 if not self._byte_parser:
79 Returns a set of line numbers, the lines that contain a match for one 102 Returns a set of line numbers, the lines that contain a match for one
80 of the regexes in `regexes`. The entire line needn't match, just a 103 of the regexes in `regexes`. The entire line needn't match, just a
81 part of it. 104 part of it.
82 105
83 """ 106 """
84 regex_c = re.compile(join_regex(regexes)) 107 combined = join_regex(regexes)
108 if env.PY2:
109 combined = combined.decode("utf8")
110 regex_c = re.compile(combined)
85 matches = set() 111 matches = set()
86 for i, ltext in enumerate(self.lines, start=1): 112 for i, ltext in enumerate(self.lines, start=1):
87 if regex_c.search(ltext): 113 if regex_c.search(ltext):
88 matches.add(i) 114 matches.add(i)
89 return matches 115 return matches
90 116
91 def _raw_parse(self): 117 def _raw_parse(self):
92 """Parse the source to find the interesting facts about its lines. 118 """Parse the source to find the interesting facts about its lines.
93 119
94 A handful of member fields are updated. 120 A handful of attributes are updated.
95 121
96 """ 122 """
97 # Find lines which match an exclusion pattern. 123 # Find lines which match an exclusion pattern.
98 if self.exclude: 124 if self.exclude:
99 self.excluded = self.lines_matching(self.exclude) 125 self.raw_excluded = self.lines_matching(self.exclude)
100 126
101 # Tokenize, to find excluded suites, to find docstrings, and to find 127 # Tokenize, to find excluded suites, to find docstrings, and to find
102 # multi-line statements. 128 # multi-line statements.
103 indent = 0 129 indent = 0
104 exclude_indent = 0 130 exclude_indent = 0
105 excluding = False 131 excluding = False
132 excluding_decorators = False
106 prev_toktype = token.INDENT 133 prev_toktype = token.INDENT
107 first_line = None 134 first_line = None
108 empty = True 135 empty = True
136 first_on_line = True
109 137
110 tokgen = generate_tokens(self.text) 138 tokgen = generate_tokens(self.text)
111 for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen: 139 for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen:
112 if self.show_tokens: # pragma: not covered 140 if self.show_tokens: # pragma: not covered
113 print("%10s %5s %-20r %r" % ( 141 print("%10s %5s %-20r %r" % (
116 )) 144 ))
117 if toktype == token.INDENT: 145 if toktype == token.INDENT:
118 indent += 1 146 indent += 1
119 elif toktype == token.DEDENT: 147 elif toktype == token.DEDENT:
120 indent -= 1 148 indent -= 1
121 elif toktype == token.NAME and ttext == 'class': 149 elif toktype == token.NAME:
122 # Class definitions look like branches in the byte code, so 150 if ttext == 'class':
123 # we need to exclude them. The simplest way is to note the 151 # Class definitions look like branches in the bytecode, so
124 # lines with the 'class' keyword. 152 # we need to exclude them. The simplest way is to note the
125 self.classdefs.add(slineno) 153 # lines with the 'class' keyword.
126 elif toktype == token.OP and ttext == ':': 154 self.raw_classdefs.add(slineno)
127 if not excluding and elineno in self.excluded: 155 elif toktype == token.OP:
128 # Start excluding a suite. We trigger off of the colon 156 if ttext == ':':
129 # token so that the #pragma comment will be recognized on 157 should_exclude = (elineno in self.raw_excluded) or excluding_decorators
130 # the same line as the colon. 158 if not excluding and should_exclude:
131 exclude_indent = indent 159 # Start excluding a suite. We trigger off of the colon
132 excluding = True 160 # token so that the #pragma comment will be recognized on
161 # the same line as the colon.
162 self.raw_excluded.add(elineno)
163 exclude_indent = indent
164 excluding = True
165 excluding_decorators = False
166 elif ttext == '@' and first_on_line:
167 # A decorator.
168 if elineno in self.raw_excluded:
169 excluding_decorators = True
170 if excluding_decorators:
171 self.raw_excluded.add(elineno)
133 elif toktype == token.STRING and prev_toktype == token.INDENT: 172 elif toktype == token.STRING and prev_toktype == token.INDENT:
134 # Strings that are first on an indented line are docstrings. 173 # Strings that are first on an indented line are docstrings.
135 # (a trick from trace.py in the stdlib.) This works for 174 # (a trick from trace.py in the stdlib.) This works for
136 # 99.9999% of cases. For the rest (!) see: 175 # 99.9999% of cases. For the rest (!) see:
137 # http://stackoverflow.com/questions/1769332/x/1769794#1769794 176 # http://stackoverflow.com/questions/1769332/x/1769794#1769794
138 self.docstrings.update(range(slineno, elineno+1)) 177 self.raw_docstrings.update(range(slineno, elineno+1))
139 elif toktype == token.NEWLINE: 178 elif toktype == token.NEWLINE:
140 if first_line is not None and elineno != first_line: 179 if first_line is not None and elineno != first_line:
141 # We're at the end of a line, and we've ended on a 180 # We're at the end of a line, and we've ended on a
142 # different line than the first line of the statement, 181 # different line than the first line of the statement,
143 # so record a multi-line range. 182 # so record a multi-line range.
144 for l in range(first_line, elineno+1): 183 for l in range(first_line, elineno+1):
145 self.multiline[l] = first_line 184 self._multiline[l] = first_line
146 first_line = None 185 first_line = None
186 first_on_line = True
147 187
148 if ttext.strip() and toktype != tokenize.COMMENT: 188 if ttext.strip() and toktype != tokenize.COMMENT:
149 # A non-whitespace token. 189 # A non-whitespace token.
150 empty = False 190 empty = False
151 if first_line is None: 191 if first_line is None:
154 first_line = slineno 194 first_line = slineno
155 # Check whether to end an excluded suite. 195 # Check whether to end an excluded suite.
156 if excluding and indent <= exclude_indent: 196 if excluding and indent <= exclude_indent:
157 excluding = False 197 excluding = False
158 if excluding: 198 if excluding:
159 self.excluded.add(elineno) 199 self.raw_excluded.add(elineno)
200 first_on_line = False
160 201
161 prev_toktype = toktype 202 prev_toktype = toktype
162 203
163 # Find the starts of the executable statements. 204 # Find the starts of the executable statements.
164 if not empty: 205 if not empty:
165 self.statement_starts.update(self.byte_parser._find_statements()) 206 self.raw_statements.update(self.byte_parser._find_statements())
166 207
167 def first_line(self, line): 208 def first_line(self, line):
168 """Return the first line number of the statement including `line`.""" 209 """Return the first line number of the statement including `line`."""
169 first_line = self.multiline.get(line) 210 return self._multiline.get(line, line)
170 if first_line:
171 return first_line
172 else:
173 return line
174 211
175 def first_lines(self, lines): 212 def first_lines(self, lines):
176 """Map the line numbers in `lines` to the correct first line of the 213 """Map the line numbers in `lines` to the correct first line of the
177 statement. 214 statement.
178 215
185 """Implement `FileReporter.translate_lines`.""" 222 """Implement `FileReporter.translate_lines`."""
186 return self.first_lines(lines) 223 return self.first_lines(lines)
187 224
188 def translate_arcs(self, arcs): 225 def translate_arcs(self, arcs):
189 """Implement `FileReporter.translate_arcs`.""" 226 """Implement `FileReporter.translate_arcs`."""
190 return [ 227 return [(self.first_line(a), self.first_line(b)) for (a, b) in arcs]
191 (self.first_line(a), self.first_line(b)) 228
192 for (a, b) in arcs
193 ]
194
195 @expensive
196 def parse_source(self): 229 def parse_source(self):
197 """Parse source text to find executable lines, excluded lines, etc. 230 """Parse source text to find executable lines, excluded lines, etc.
198 231
199 Return values are 1) a set of executable line numbers, and 2) a set of 232 Sets the .excluded and .statements attributes, normalized to the first
200 excluded line numbers. 233 line of multi-line statements.
201
202 Reported line numbers are normalized to the first line of multi-line
203 statements.
204 234
205 """ 235 """
206 try: 236 try:
207 self._raw_parse() 237 self._raw_parse()
208 except (tokenize.TokenError, IndentationError) as err: 238 except (tokenize.TokenError, IndentationError) as err:
209 if hasattr(err, "lineno"): 239 if hasattr(err, "lineno"):
210 lineno = err.lineno # IndentationError 240 lineno = err.lineno # IndentationError
211 else: 241 else:
212 lineno = err.args[1][0] # TokenError 242 lineno = err.args[1][0] # TokenError
213 raise NotPython( 243 raise NotPython(
214 "Couldn't parse '%s' as Python source: '%s' at line %d" % ( 244 u"Couldn't parse '%s' as Python source: '%s' at line %d" % (
215 self.filename, err.args[0], lineno 245 self.filename, err.args[0], lineno
216 ) 246 )
217 ) 247 )
218 248
219 excluded_lines = self.first_lines(self.excluded) 249 self.excluded = self.first_lines(self.raw_excluded)
220 ignore = set() 250
221 ignore.update(excluded_lines) 251 ignore = self.excluded | self.raw_docstrings
222 ignore.update(self.docstrings) 252 starts = self.raw_statements - ignore
223 starts = self.statement_starts - ignore 253 self.statements = self.first_lines(starts) - ignore
224 lines = self.first_lines(starts)
225 lines -= ignore
226
227 return lines, excluded_lines
228 254
229 def arcs(self): 255 def arcs(self):
230 """Get information about the arcs available in the code. 256 """Get information about the arcs available in the code.
231 257
232 Returns a set of line number pairs. Line numbers have been normalized 258 Returns a set of line number pairs. Line numbers have been normalized
233 to the first line of multi-line statements. 259 to the first line of multi-line statements.
234 260
235 """ 261 """
236 if self._all_arcs is None: 262 if self._all_arcs is None:
237 self._all_arcs = set() 263 self._analyze_ast()
238 for l1, l2 in self.byte_parser._all_arcs():
239 fl1 = self.first_line(l1)
240 fl2 = self.first_line(l2)
241 if fl1 != fl2:
242 self._all_arcs.add((fl1, fl2))
243 return self._all_arcs 264 return self._all_arcs
265
266 def _analyze_ast(self):
267 """Run the AstArcAnalyzer and save its results.
268
269 `_all_arcs` is the set of arcs in the code.
270
271 """
272 aaa = AstArcAnalyzer(self.text, self.raw_statements, self._multiline)
273 aaa.analyze()
274
275 self._all_arcs = set()
276 for l1, l2 in aaa.arcs:
277 fl1 = self.first_line(l1)
278 fl2 = self.first_line(l2)
279 if fl1 != fl2:
280 self._all_arcs.add((fl1, fl2))
281
282 self._missing_arc_fragments = aaa.missing_arc_fragments
244 283
245 def exit_counts(self): 284 def exit_counts(self):
246 """Get a count of exits from that each line. 285 """Get a count of exits from that each line.
247 286
248 Excluded lines are excluded. 287 Excluded lines are excluded.
249 288
250 """ 289 """
251 excluded_lines = self.first_lines(self.excluded)
252 exit_counts = collections.defaultdict(int) 290 exit_counts = collections.defaultdict(int)
253 for l1, l2 in self.arcs(): 291 for l1, l2 in self.arcs():
254 if l1 < 0: 292 if l1 < 0:
255 # Don't ever report -1 as a line number 293 # Don't ever report -1 as a line number
256 continue 294 continue
257 if l1 in excluded_lines: 295 if l1 in self.excluded:
258 # Don't report excluded lines as line numbers. 296 # Don't report excluded lines as line numbers.
259 continue 297 continue
260 if l2 in excluded_lines: 298 if l2 in self.excluded:
261 # Arcs to excluded lines shouldn't count. 299 # Arcs to excluded lines shouldn't count.
262 continue 300 continue
263 exit_counts[l1] += 1 301 exit_counts[l1] += 1
264 302
265 # Class definitions have one extra exit, so remove one for each: 303 # Class definitions have one extra exit, so remove one for each:
266 for l in self.classdefs: 304 for l in self.raw_classdefs:
267 # Ensure key is there: class definitions can include excluded lines. 305 # Ensure key is there: class definitions can include excluded lines.
268 if l in exit_counts: 306 if l in exit_counts:
269 exit_counts[l] -= 1 307 exit_counts[l] -= 1
270 308
271 return exit_counts 309 return exit_counts
272 310
273 311 def missing_arc_description(self, start, end, executed_arcs=None):
274 ## Opcodes that guide the ByteParser. 312 """Provide an English sentence describing a missing arc."""
275 313 if self._missing_arc_fragments is None:
276 def _opcode(name): 314 self._analyze_ast()
277 """Return the opcode by name from the dis module.""" 315
278 return dis.opmap[name] 316 actual_start = start
279 317
280 318 if (
281 def _opcode_set(*names): 319 executed_arcs and
282 """Return a set of opcodes by the names in `names`.""" 320 end < 0 and end == -start and
283 s = set() 321 (end, start) not in executed_arcs and
284 for name in names: 322 (end, start) in self._missing_arc_fragments
285 try: 323 ):
286 s.add(_opcode(name)) 324 # It's a one-line callable, and we never even started it,
287 except KeyError: 325 # and we have a message about not starting it.
288 pass 326 start, end = end, start
289 return s 327
290 328 fragment_pairs = self._missing_arc_fragments.get((start, end), [(None, None)])
291 # Opcodes that leave the code object. 329
292 OPS_CODE_END = _opcode_set('RETURN_VALUE') 330 msgs = []
293 331 for fragment_pair in fragment_pairs:
294 # Opcodes that unconditionally end the code chunk. 332 smsg, emsg = fragment_pair
295 OPS_CHUNK_END = _opcode_set( 333
296 'JUMP_ABSOLUTE', 'JUMP_FORWARD', 'RETURN_VALUE', 'RAISE_VARARGS', 334 if emsg is None:
297 'BREAK_LOOP', 'CONTINUE_LOOP', 335 if end < 0:
298 ) 336 # Hmm, maybe we have a one-line callable, let's check.
299 337 if (-end, end) in self._missing_arc_fragments:
300 # Opcodes that unconditionally begin a new code chunk. By starting new chunks 338 return self.missing_arc_description(-end, end)
301 # with unconditional jump instructions, we neatly deal with jumps to jumps 339 emsg = "didn't jump to the function exit"
302 # properly. 340 else:
303 OPS_CHUNK_BEGIN = _opcode_set('JUMP_ABSOLUTE', 'JUMP_FORWARD') 341 emsg = "didn't jump to line {lineno}"
304 342 emsg = emsg.format(lineno=end)
305 # Opcodes that push a block on the block stack. 343
306 OPS_PUSH_BLOCK = _opcode_set( 344 msg = "line {start} {emsg}".format(start=actual_start, emsg=emsg)
307 'SETUP_LOOP', 'SETUP_EXCEPT', 'SETUP_FINALLY', 'SETUP_WITH' 345 if smsg is not None:
308 ) 346 msg += ", because {smsg}".format(smsg=smsg.format(lineno=actual_start))
309 347
310 # Block types for exception handling. 348 msgs.append(msg)
311 OPS_EXCEPT_BLOCKS = _opcode_set('SETUP_EXCEPT', 'SETUP_FINALLY') 349
312 350 return " or ".join(msgs)
313 # Opcodes that pop a block from the block stack.
314 OPS_POP_BLOCK = _opcode_set('POP_BLOCK')
315
316 # Opcodes that have a jump destination, but aren't really a jump.
317 OPS_NO_JUMP = OPS_PUSH_BLOCK
318
319 # Individual opcodes we need below.
320 OP_BREAK_LOOP = _opcode('BREAK_LOOP')
321 OP_END_FINALLY = _opcode('END_FINALLY')
322 OP_COMPARE_OP = _opcode('COMPARE_OP')
323 COMPARE_EXCEPTION = 10 # just have to get this constant from the code.
324 OP_LOAD_CONST = _opcode('LOAD_CONST')
325 OP_RETURN_VALUE = _opcode('RETURN_VALUE')
326 351
327 352
328 class ByteParser(object): 353 class ByteParser(object):
329 """Parse byte codes to understand the structure of code.""" 354 """Parse bytecode to understand the structure of code."""
330 355
331 @contract(text='unicode') 356 @contract(text='unicode')
332 def __init__(self, text, code=None, filename=None): 357 def __init__(self, text, code=None, filename=None):
333 self.text = text 358 self.text = text
334 if code: 359 if code:
336 else: 361 else:
337 try: 362 try:
338 self.code = compile_unicode(text, filename, "exec") 363 self.code = compile_unicode(text, filename, "exec")
339 except SyntaxError as synerr: 364 except SyntaxError as synerr:
340 raise NotPython( 365 raise NotPython(
341 "Couldn't parse '%s' as Python source: '%s' at line %d" % ( 366 u"Couldn't parse '%s' as Python source: '%s' at line %d" % (
342 filename, synerr.msg, synerr.lineno 367 filename, synerr.msg, synerr.lineno
343 ) 368 )
344 ) 369 )
345 370
346 # Alternative Python implementations don't always provide all the 371 # Alternative Python implementations don't always provide all the
347 # attributes on code objects that we need to do the analysis. 372 # attributes on code objects that we need to do the analysis.
348 for attr in ['co_lnotab', 'co_firstlineno', 'co_consts', 'co_code']: 373 for attr in ['co_lnotab', 'co_firstlineno', 'co_consts']:
349 if not hasattr(self.code, attr): 374 if not hasattr(self.code, attr):
350 raise CoverageException( 375 raise CoverageException(
351 "This implementation of Python doesn't support code analysis.\n" 376 "This implementation of Python doesn't support code analysis.\n"
352 "Run coverage.py under CPython for this command." 377 "Run coverage.py under CPython for this command."
353 ) 378 )
398 for bp in self.child_parsers(): 423 for bp in self.child_parsers():
399 # Get all of the lineno information from this code. 424 # Get all of the lineno information from this code.
400 for _, l in bp._bytes_lines(): 425 for _, l in bp._bytes_lines():
401 yield l 426 yield l
402 427
403 def _block_stack_repr(self, block_stack): # pragma: debugging 428
404 """Get a string version of `block_stack`, for debugging.""" 429 #
405 blocks = ", ".join( 430 # AST analysis
406 "(%s, %r)" % (dis.opname[b[0]], b[1]) for b in block_stack 431 #
407 ) 432
408 return "[" + blocks + "]" 433 class LoopBlock(object):
409 434 """A block on the block stack representing a `for` or `while` loop."""
410 def _split_into_chunks(self): 435 def __init__(self, start):
411 """Split the code object into a list of `Chunk` objects. 436 self.start = start
412 437 self.break_exits = set()
413 Each chunk is only entered at its first instruction, though there can 438
414 be many exits from a chunk. 439
415 440 class FunctionBlock(object):
416 Returns a list of `Chunk` objects. 441 """A block on the block stack representing a function definition."""
417 442 def __init__(self, start, name):
418 """ 443 self.start = start
419 # The list of chunks so far, and the one we're working on. 444 self.name = name
420 chunks = [] 445
421 chunk = None 446
422 447 class TryBlock(object):
423 # A dict mapping byte offsets of line starts to the line numbers. 448 """A block on the block stack representing a `try` block."""
424 bytes_lines_map = dict(self._bytes_lines()) 449 def __init__(self, handler_start=None, final_start=None):
425 450 self.handler_start = handler_start
426 # The block stack: loops and try blocks get pushed here for the 451 self.final_start = final_start
427 # implicit jumps that can occur. 452 self.break_from = set()
428 # Each entry is a tuple: (block type, destination) 453 self.continue_from = set()
429 block_stack = [] 454 self.return_from = set()
430 455 self.raise_from = set()
431 # Some op codes are followed by branches that should be ignored. This 456
432 # is a count of how many ignores are left. 457
433 ignore_branch = 0 458 class ArcStart(collections.namedtuple("Arc", "lineno, cause")):
434 459 """The information needed to start an arc.
435 # We have to handle the last two bytecodes specially. 460
436 ult = penult = None 461 `lineno` is the line number the arc starts from. `cause` is a fragment
437 462 used as the startmsg for AstArcAnalyzer.missing_arc_fragments.
438 # Get a set of all of the jump-to points. 463
439 jump_to = set() 464 """
440 bytecodes = list(ByteCodes(self.code.co_code)) 465 def __new__(cls, lineno, cause=None):
441 for bc in bytecodes: 466 return super(ArcStart, cls).__new__(cls, lineno, cause)
442 if bc.jump_to >= 0: 467
443 jump_to.add(bc.jump_to) 468
444 469 # Define contract words that PyContract doesn't have.
445 chunk_lineno = 0 470 # ArcStarts is for a list or set of ArcStart's.
446 471 new_contract('ArcStarts', lambda seq: all(isinstance(x, ArcStart) for x in seq))
447 # Walk the byte codes building chunks. 472
448 for bc in bytecodes: 473
449 # Maybe have to start a new chunk. 474 class AstArcAnalyzer(object):
450 start_new_chunk = False 475 """Analyze source text with an AST to find executable code paths."""
451 first_chunk = False 476
452 if bc.offset in bytes_lines_map: 477 @contract(text='unicode', statements=set)
453 # Start a new chunk for each source line number. 478 def __init__(self, text, statements, multiline):
454 start_new_chunk = True 479 self.root_node = ast.parse(neuter_encoding_declaration(text))
455 chunk_lineno = bytes_lines_map[bc.offset] 480 # TODO: I think this is happening in too many places.
456 first_chunk = True 481 self.statements = set(multiline.get(l, l) for l in statements)
457 elif bc.offset in jump_to: 482 self.multiline = multiline
458 # To make chunks have a single entrance, we have to make a new 483
459 # chunk when we get to a place some bytecode jumps to. 484 if int(os.environ.get("COVERAGE_ASTDUMP", 0)): # pragma: debugging
460 start_new_chunk = True 485 # Dump the AST so that failing tests have helpful output.
461 elif bc.op in OPS_CHUNK_BEGIN: 486 print("Statements: {}".format(self.statements))
462 # Jumps deserve their own unnumbered chunk. This fixes 487 print("Multiline map: {}".format(self.multiline))
463 # problems with jumps to jumps getting confused. 488 ast_dump(self.root_node)
464 start_new_chunk = True 489
465 490 self.arcs = set()
466 if not chunk or start_new_chunk: 491
467 if chunk: 492 # A map from arc pairs to a pair of sentence fragments: (startmsg, endmsg).
468 chunk.exits.add(bc.offset) 493 # For an arc from line 17, they should be usable like:
469 chunk = Chunk(bc.offset, chunk_lineno, first_chunk) 494 # "Line 17 {endmsg}, because {startmsg}"
470 if not chunks: 495 self.missing_arc_fragments = collections.defaultdict(list)
471 # The very first chunk of a code object is always an 496 self.block_stack = []
472 # entrance. 497
473 chunk.entrance = True 498 self.debug = bool(int(os.environ.get("COVERAGE_TRACK_ARCS", 0)))
474 chunks.append(chunk) 499
475 500 def analyze(self):
476 # Look at the opcode. 501 """Examine the AST tree from `root_node` to determine possible arcs.
477 if bc.jump_to >= 0 and bc.op not in OPS_NO_JUMP: 502
478 if ignore_branch: 503 This sets the `arcs` attribute to be a set of (from, to) line number
479 # Someone earlier wanted us to ignore this branch. 504 pairs.
480 ignore_branch -= 1 505
481 else: 506 """
482 # The opcode has a jump, it's an exit for this chunk. 507 for node in ast.walk(self.root_node):
483 chunk.exits.add(bc.jump_to) 508 node_name = node.__class__.__name__
484 509 code_object_handler = getattr(self, "_code_object__" + node_name, None)
485 if bc.op in OPS_CODE_END: 510 if code_object_handler is not None:
486 # The opcode can exit the code object. 511 code_object_handler(node)
487 chunk.exits.add(-self.code.co_firstlineno) 512
488 if bc.op in OPS_PUSH_BLOCK: 513 def add_arc(self, start, end, smsg=None, emsg=None):
489 # The opcode adds a block to the block_stack. 514 """Add an arc, including message fragments to use if it is missing."""
490 block_stack.append((bc.op, bc.jump_to)) 515 if self.debug:
491 if bc.op in OPS_POP_BLOCK: 516 print("\nAdding arc: ({}, {}): {!r}, {!r}".format(start, end, smsg, emsg))
492 # The opcode pops a block from the block stack. 517 print(short_stack(limit=6))
493 block_stack.pop() 518 self.arcs.add((start, end))
494 if bc.op in OPS_CHUNK_END: 519
495 # This opcode forces the end of the chunk. 520 if smsg is not None or emsg is not None:
496 if bc.op == OP_BREAK_LOOP: 521 self.missing_arc_fragments[(start, end)].append((smsg, emsg))
497 # A break is implicit: jump where the top of the 522
498 # block_stack points. 523 def nearest_blocks(self):
499 chunk.exits.add(block_stack[-1][1]) 524 """Yield the blocks in nearest-to-farthest order."""
500 chunk = None 525 return reversed(self.block_stack)
501 if bc.op == OP_END_FINALLY: 526
502 # For the finally clause we need to find the closest exception 527 @contract(returns=int)
503 # block, and use its jump target as an exit. 528 def line_for_node(self, node):
504 for block in reversed(block_stack): 529 """What is the right line number to use for this node?
505 if block[0] in OPS_EXCEPT_BLOCKS: 530
506 chunk.exits.add(block[1]) 531 This dispatches to _line__Node functions where needed.
507 break 532
508 if bc.op == OP_COMPARE_OP and bc.arg == COMPARE_EXCEPTION: 533 """
509 # This is an except clause. We want to overlook the next 534 node_name = node.__class__.__name__
510 # branch, so that except's don't count as branches. 535 handler = getattr(self, "_line__" + node_name, None)
511 ignore_branch += 1 536 if handler is not None:
512 537 return handler(node)
513 penult = ult 538 else:
514 ult = bc 539 return node.lineno
515 540
516 if chunks: 541 def _line__Assign(self, node):
517 # The last two bytecodes could be a dummy "return None" that 542 return self.line_for_node(node.value)
518 # shouldn't be counted as real code. Every Python code object seems 543
519 # to end with a return, and a "return None" is inserted if there 544 def _line__Dict(self, node):
520 # isn't an explicit return in the source. 545 # Python 3.5 changed how dict literals are made.
521 if ult and penult: 546 if env.PYVERSION >= (3, 5) and node.keys:
522 if penult.op == OP_LOAD_CONST and ult.op == OP_RETURN_VALUE: 547 if node.keys[0] is not None:
523 if self.code.co_consts[penult.arg] is None: 548 return node.keys[0].lineno
524 # This is "return None", but is it dummy? A real line 549 else:
525 # would be a last chunk all by itself. 550 # Unpacked dict literals `{**{'a':1}}` have None as the key,
526 if chunks[-1].byte != penult.offset: 551 # use the value in that case.
527 ex = -self.code.co_firstlineno 552 return node.values[0].lineno
528 # Split the last chunk 553 else:
529 last_chunk = chunks[-1] 554 return node.lineno
530 last_chunk.exits.remove(ex) 555
531 last_chunk.exits.add(penult.offset) 556 def _line__List(self, node):
532 chunk = Chunk( 557 if node.elts:
533 penult.offset, last_chunk.line, False 558 return self.line_for_node(node.elts[0])
534 ) 559 else:
535 chunk.exits.add(ex) 560 return node.lineno
536 chunks.append(chunk) 561
537 562 def _line__Module(self, node):
538 # Give all the chunks a length. 563 if node.body:
539 chunks[-1].length = bc.next_offset - chunks[-1].byte 564 return self.line_for_node(node.body[0])
540 for i in range(len(chunks)-1): 565 else:
541 chunks[i].length = chunks[i+1].byte - chunks[i].byte 566 # Modules have no line number, they always start at 1.
542 567 return 1
543 #self.validate_chunks(chunks) 568
544 return chunks 569 OK_TO_DEFAULT = set([
545 570 "Assign", "Assert", "AugAssign", "Delete", "Exec", "Expr", "Global",
546 def validate_chunks(self, chunks): # pragma: debugging 571 "Import", "ImportFrom", "Nonlocal", "Pass", "Print",
547 """Validate the rule that chunks have a single entrance.""" 572 ])
548 # starts is the entrances to the chunks 573
549 starts = set(ch.byte for ch in chunks) 574 @contract(returns='ArcStarts')
550 for ch in chunks: 575 def add_arcs(self, node):
551 assert all((ex in starts or ex < 0) for ex in ch.exits) 576 """Add the arcs for `node`.
552 577
553 def _arcs(self): 578 Return a set of ArcStarts, exits from this node to the next.
554 """Find the executable arcs in the code. 579
555 580 """
556 Yields pairs: (from,to). From and to are integer line numbers. If 581 node_name = node.__class__.__name__
557 from is < 0, then the arc is an entrance into the code object. If to 582 handler = getattr(self, "_handle__" + node_name, None)
558 is < 0, the arc is an exit from the code object. 583 if handler is not None:
559 584 return handler(node)
560 """ 585
561 chunks = self._split_into_chunks() 586 if 0:
562 587 node_name = node.__class__.__name__
563 # A map from byte offsets to the chunk starting at that offset. 588 if node_name not in self.OK_TO_DEFAULT:
564 byte_chunks = dict((c.byte, c) for c in chunks) 589 print("*** Unhandled: {0}".format(node))
565 590 return set([ArcStart(self.line_for_node(node), cause=None)])
566 # Traverse from the first chunk in each line, and yield arcs where 591
567 # the trace function will be invoked. 592 @contract(returns='ArcStarts')
568 for chunk in chunks: 593 def add_body_arcs(self, body, from_start=None, prev_starts=None):
569 if chunk.entrance: 594 """Add arcs for the body of a compound statement.
570 yield (-1, chunk.line) 595
571 596 `body` is the body node. `from_start` is a single `ArcStart` that can
572 if not chunk.first: 597 be the previous line in flow before this body. `prev_starts` is a set
598 of ArcStarts that can be the previous line. Only one of them should be
599 given.
600
601 Returns a set of ArcStarts, the exits from this body.
602
603 """
604 if prev_starts is None:
605 prev_starts = set([from_start])
606 for body_node in body:
607 lineno = self.line_for_node(body_node)
608 first_line = self.multiline.get(lineno, lineno)
609 if first_line not in self.statements:
573 continue 610 continue
574 611 for prev_start in prev_starts:
575 chunks_considered = set() 612 self.add_arc(prev_start.lineno, lineno, prev_start.cause)
576 chunks_to_consider = [chunk] 613 prev_starts = self.add_arcs(body_node)
577 while chunks_to_consider: 614 return prev_starts
578 # Get the chunk we're considering, and make sure we don't 615
579 # consider it again. 616 def is_constant_expr(self, node):
580 this_chunk = chunks_to_consider.pop() 617 """Is this a compile-time constant?"""
581 chunks_considered.add(this_chunk) 618 node_name = node.__class__.__name__
582 619 if node_name in ["NameConstant", "Num"]:
583 # For each exit, add the line number if the trace function 620 return True
584 # would be triggered, or add the chunk to those being 621 elif node_name == "Name":
585 # considered if not. 622 if env.PY3 and node.id in ["True", "False", "None"]:
586 for ex in this_chunk.exits: 623 return True
587 if ex < 0: 624 return False
588 yield (chunk.line, ex) 625
589 else: 626 # tests to write:
590 next_chunk = byte_chunks[ex] 627 # TODO: while EXPR:
591 if next_chunk in chunks_considered: 628 # TODO: while False:
592 continue 629 # TODO: listcomps hidden deep in other expressions
593 630 # TODO: listcomps hidden in lists: x = [[i for i in range(10)]]
594 # The trace function is invoked if visiting the first 631 # TODO: nested function definitions
595 # bytecode in a line, or if the transition is a 632
596 # backward jump. 633 @contract(exits='ArcStarts')
597 backward_jump = next_chunk.byte < this_chunk.byte 634 def process_break_exits(self, exits):
598 if next_chunk.first or backward_jump: 635 """Add arcs due to jumps from `exits` being breaks."""
599 if next_chunk.line != chunk.line: 636 for block in self.nearest_blocks():
600 yield (chunk.line, next_chunk.line) 637 if isinstance(block, LoopBlock):
601 else: 638 block.break_exits.update(exits)
602 chunks_to_consider.append(next_chunk) 639 break
603 640 elif isinstance(block, TryBlock) and block.final_start is not None:
604 def _all_chunks(self): 641 block.break_from.update(exits)
605 """Returns a list of `Chunk` objects for this code and its children. 642 break
606 643
607 See `_split_into_chunks` for details. 644 @contract(exits='ArcStarts')
608 645 def process_continue_exits(self, exits):
609 """ 646 """Add arcs due to jumps from `exits` being continues."""
610 chunks = [] 647 for block in self.nearest_blocks():
611 for bp in self.child_parsers(): 648 if isinstance(block, LoopBlock):
612 chunks.extend(bp._split_into_chunks()) 649 for xit in exits:
613 650 self.add_arc(xit.lineno, block.start, xit.cause)
614 return chunks 651 break
615 652 elif isinstance(block, TryBlock) and block.final_start is not None:
616 def _all_arcs(self): 653 block.continue_from.update(exits)
617 """Get the set of all arcs in this code object and its children. 654 break
618 655
619 See `_arcs` for details. 656 @contract(exits='ArcStarts')
620 657 def process_raise_exits(self, exits):
621 """ 658 """Add arcs due to jumps from `exits` being raises."""
622 arcs = set() 659 for block in self.nearest_blocks():
623 for bp in self.child_parsers(): 660 if isinstance(block, TryBlock):
624 arcs.update(bp._arcs()) 661 if block.handler_start is not None:
625 662 for xit in exits:
626 return arcs 663 self.add_arc(xit.lineno, block.handler_start, xit.cause)
627 664 break
628 665 elif block.final_start is not None:
629 class Chunk(object): 666 block.raise_from.update(exits)
630 """A sequence of byte codes with a single entrance. 667 break
631 668 elif isinstance(block, FunctionBlock):
632 To analyze byte code, we have to divide it into chunks, sequences of byte 669 for xit in exits:
633 codes such that each chunk has only one entrance, the first instruction in 670 self.add_arc(
634 the block. 671 xit.lineno, -block.start, xit.cause,
635 672 "didn't except from function '{0}'".format(block.name),
636 This is almost the CS concept of `basic block`_, except that we're willing 673 )
637 to have many exits from a chunk, and "basic block" is a more cumbersome 674 break
638 term. 675
639 676 @contract(exits='ArcStarts')
640 .. _basic block: http://en.wikipedia.org/wiki/Basic_block 677 def process_return_exits(self, exits):
641 678 """Add arcs due to jumps from `exits` being returns."""
642 `byte` is the offset to the bytecode starting this chunk. 679 for block in self.nearest_blocks():
643 680 if isinstance(block, TryBlock) and block.final_start is not None:
644 `line` is the source line number containing this chunk. 681 block.return_from.update(exits)
645 682 break
646 `first` is true if this is the first chunk in the source line. 683 elif isinstance(block, FunctionBlock):
647 684 for xit in exits:
648 An exit < 0 means the chunk can leave the code (return). The exit is 685 self.add_arc(
649 the negative of the starting line number of the code block. 686 xit.lineno, -block.start, xit.cause,
650 687 "didn't return from function '{0}'".format(block.name),
651 The `entrance` attribute is a boolean indicating whether the code object 688 )
652 can be entered at this chunk. 689 break
690
691 ## Handlers
692
693 @contract(returns='ArcStarts')
694 def _handle__Break(self, node):
695 here = self.line_for_node(node)
696 break_start = ArcStart(here, cause="the break on line {lineno} wasn't executed")
697 self.process_break_exits([break_start])
698 return set()
699
700 @contract(returns='ArcStarts')
701 def _handle_decorated(self, node):
702 """Add arcs for things that can be decorated (classes and functions)."""
703 last = self.line_for_node(node)
704 if node.decorator_list:
705 for dec_node in node.decorator_list:
706 dec_start = self.line_for_node(dec_node)
707 if dec_start != last:
708 self.add_arc(last, dec_start)
709 last = dec_start
710 # The definition line may have been missed, but we should have it
711 # in `self.statements`. For some constructs, `line_for_node` is
712 # not what we'd think of as the first line in the statement, so map
713 # it to the first one.
714 body_start = self.line_for_node(node.body[0])
715 body_start = self.multiline.get(body_start, body_start)
716 for lineno in range(last+1, body_start):
717 if lineno in self.statements:
718 self.add_arc(last, lineno)
719 last = lineno
720 # The body is handled in collect_arcs.
721 return set([ArcStart(last, cause=None)])
722
723 _handle__ClassDef = _handle_decorated
724
725 @contract(returns='ArcStarts')
726 def _handle__Continue(self, node):
727 here = self.line_for_node(node)
728 continue_start = ArcStart(here, cause="the continue on line {lineno} wasn't executed")
729 self.process_continue_exits([continue_start])
730 return set()
731
732 @contract(returns='ArcStarts')
733 def _handle__For(self, node):
734 start = self.line_for_node(node.iter)
735 self.block_stack.append(LoopBlock(start=start))
736 from_start = ArcStart(start, cause="the loop on line {lineno} never started")
737 exits = self.add_body_arcs(node.body, from_start=from_start)
738 # Any exit from the body will go back to the top of the loop.
739 for xit in exits:
740 self.add_arc(xit.lineno, start, xit.cause)
741 my_block = self.block_stack.pop()
742 exits = my_block.break_exits
743 from_start = ArcStart(start, cause="the loop on line {lineno} didn't complete")
744 if node.orelse:
745 else_exits = self.add_body_arcs(node.orelse, from_start=from_start)
746 exits |= else_exits
747 else:
748 # no else clause: exit from the for line.
749 exits.add(from_start)
750 return exits
751
752 _handle__AsyncFor = _handle__For
753
754 _handle__FunctionDef = _handle_decorated
755 _handle__AsyncFunctionDef = _handle_decorated
756
757 @contract(returns='ArcStarts')
758 def _handle__If(self, node):
759 start = self.line_for_node(node.test)
760 from_start = ArcStart(start, cause="the condition on line {lineno} was never true")
761 exits = self.add_body_arcs(node.body, from_start=from_start)
762 from_start = ArcStart(start, cause="the condition on line {lineno} was never false")
763 exits |= self.add_body_arcs(node.orelse, from_start=from_start)
764 return exits
765
766 @contract(returns='ArcStarts')
767 def _handle__Raise(self, node):
768 here = self.line_for_node(node)
769 raise_start = ArcStart(here, cause="the raise on line {lineno} wasn't executed")
770 self.process_raise_exits([raise_start])
771 # `raise` statement jumps away, no exits from here.
772 return set()
773
774 @contract(returns='ArcStarts')
775 def _handle__Return(self, node):
776 here = self.line_for_node(node)
777 return_start = ArcStart(here, cause="the return on line {lineno} wasn't executed")
778 self.process_return_exits([return_start])
779 # `return` statement jumps away, no exits from here.
780 return set()
781
782 @contract(returns='ArcStarts')
783 def _handle__Try(self, node):
784 if node.handlers:
785 handler_start = self.line_for_node(node.handlers[0])
786 else:
787 handler_start = None
788
789 if node.finalbody:
790 final_start = self.line_for_node(node.finalbody[0])
791 else:
792 final_start = None
793
794 try_block = TryBlock(handler_start=handler_start, final_start=final_start)
795 self.block_stack.append(try_block)
796
797 start = self.line_for_node(node)
798 exits = self.add_body_arcs(node.body, from_start=ArcStart(start, cause=None))
799
800 # We're done with the `try` body, so this block no longer handles
801 # exceptions. We keep the block so the `finally` clause can pick up
802 # flows from the handlers and `else` clause.
803 if node.finalbody:
804 try_block.handler_start = None
805 if node.handlers:
806 # If there are `except` clauses, then raises in the try body
807 # will already jump to them. Start this set over for raises in
808 # `except` and `else`.
809 try_block.raise_from = set([])
810 else:
811 self.block_stack.pop()
812
813 handler_exits = set()
814
815 if node.handlers:
816 last_handler_start = None
817 for handler_node in node.handlers:
818 handler_start = self.line_for_node(handler_node)
819 if last_handler_start is not None:
820 self.add_arc(last_handler_start, handler_start)
821 last_handler_start = handler_start
822 from_cause = "the exception caught by line {lineno} didn't happen"
823 from_start = ArcStart(handler_start, cause=from_cause)
824 handler_exits |= self.add_body_arcs(handler_node.body, from_start=from_start)
825
826 if node.orelse:
827 exits = self.add_body_arcs(node.orelse, prev_starts=exits)
828
829 exits |= handler_exits
830
831 if node.finalbody:
832 self.block_stack.pop()
833 final_from = ( # You can get to the `finally` clause from:
834 exits | # the exits of the body or `else` clause,
835 try_block.break_from | # or a `break`,
836 try_block.continue_from | # or a `continue`,
837 try_block.raise_from | # or a `raise`,
838 try_block.return_from # or a `return`.
839 )
840
841 exits = self.add_body_arcs(node.finalbody, prev_starts=final_from)
842 if try_block.break_from:
843 break_exits = self._combine_finally_starts(try_block.break_from, exits)
844 self.process_break_exits(break_exits)
845 if try_block.continue_from:
846 continue_exits = self._combine_finally_starts(try_block.continue_from, exits)
847 self.process_continue_exits(continue_exits)
848 if try_block.raise_from:
849 raise_exits = self._combine_finally_starts(try_block.raise_from, exits)
850 self.process_raise_exits(raise_exits)
851 if try_block.return_from:
852 return_exits = self._combine_finally_starts(try_block.return_from, exits)
853 self.process_return_exits(return_exits)
854
855 return exits
856
857 def _combine_finally_starts(self, starts, exits):
858 """Helper for building the cause of `finally` branches."""
859 causes = []
860 for lineno, cause in sorted(starts):
861 if cause is not None:
862 causes.append(cause.format(lineno=lineno))
863 cause = " or ".join(causes)
864 exits = set(ArcStart(ex.lineno, cause) for ex in exits)
865 return exits
866
867 @contract(returns='ArcStarts')
868 def _handle__TryExcept(self, node):
869 # Python 2.7 uses separate TryExcept and TryFinally nodes. If we get
870 # TryExcept, it means there was no finally, so fake it, and treat as
871 # a general Try node.
872 node.finalbody = []
873 return self._handle__Try(node)
874
875 @contract(returns='ArcStarts')
876 def _handle__TryFinally(self, node):
877 # Python 2.7 uses separate TryExcept and TryFinally nodes. If we get
878 # TryFinally, see if there's a TryExcept nested inside. If so, merge
879 # them. Otherwise, fake fields to complete a Try node.
880 node.handlers = []
881 node.orelse = []
882
883 first = node.body[0]
884 if first.__class__.__name__ == "TryExcept" and node.lineno == first.lineno:
885 assert len(node.body) == 1
886 node.body = first.body
887 node.handlers = first.handlers
888 node.orelse = first.orelse
889
890 return self._handle__Try(node)
891
892 @contract(returns='ArcStarts')
893 def _handle__While(self, node):
894 constant_test = self.is_constant_expr(node.test)
895 start = to_top = self.line_for_node(node.test)
896 if constant_test:
897 to_top = self.line_for_node(node.body[0])
898 self.block_stack.append(LoopBlock(start=start))
899 from_start = ArcStart(start, cause="the condition on line {lineno} was never true")
900 exits = self.add_body_arcs(node.body, from_start=from_start)
901 for xit in exits:
902 self.add_arc(xit.lineno, to_top, xit.cause)
903 exits = set()
904 my_block = self.block_stack.pop()
905 exits.update(my_block.break_exits)
906 from_start = ArcStart(start, cause="the condition on line {lineno} was never false")
907 if node.orelse:
908 else_exits = self.add_body_arcs(node.orelse, from_start=from_start)
909 exits |= else_exits
910 else:
911 # No `else` clause: you can exit from the start.
912 if not constant_test:
913 exits.add(from_start)
914 return exits
915
916 @contract(returns='ArcStarts')
917 def _handle__With(self, node):
918 start = self.line_for_node(node)
919 exits = self.add_body_arcs(node.body, from_start=ArcStart(start))
920 return exits
921
922 _handle__AsyncWith = _handle__With
923
924 def _code_object__Module(self, node):
925 start = self.line_for_node(node)
926 if node.body:
927 exits = self.add_body_arcs(node.body, from_start=ArcStart(-start))
928 for xit in exits:
929 self.add_arc(xit.lineno, -start, xit.cause, "didn't exit the module")
930 else:
931 # Empty module.
932 self.add_arc(-start, start)
933 self.add_arc(start, -start)
934
935 def _code_object__FunctionDef(self, node):
936 start = self.line_for_node(node)
937 self.block_stack.append(FunctionBlock(start=start, name=node.name))
938 exits = self.add_body_arcs(node.body, from_start=ArcStart(-start))
939 self.process_return_exits(exits)
940 self.block_stack.pop()
941
942 _code_object__AsyncFunctionDef = _code_object__FunctionDef
943
944 def _code_object__ClassDef(self, node):
945 start = self.line_for_node(node)
946 self.add_arc(-start, start)
947 exits = self.add_body_arcs(node.body, from_start=ArcStart(start))
948 for xit in exits:
949 self.add_arc(
950 xit.lineno, -start, xit.cause,
951 "didn't exit the body of class '{0}'".format(node.name),
952 )
953
954 def _make_oneline_code_method(noun): # pylint: disable=no-self-argument
955 """A function to make methods for online callable _code_object__ methods."""
956 def _code_object__oneline_callable(self, node):
957 start = self.line_for_node(node)
958 self.add_arc(-start, start, None, "didn't run the {0} on line {1}".format(noun, start))
959 self.add_arc(
960 start, -start, None,
961 "didn't finish the {0} on line {1}".format(noun, start),
962 )
963 return _code_object__oneline_callable
964
965 _code_object__Lambda = _make_oneline_code_method("lambda")
966 _code_object__GeneratorExp = _make_oneline_code_method("generator expression")
967 _code_object__DictComp = _make_oneline_code_method("dictionary comprehension")
968 _code_object__SetComp = _make_oneline_code_method("set comprehension")
969 if env.PY3:
970 _code_object__ListComp = _make_oneline_code_method("list comprehension")
971
972
973 SKIP_DUMP_FIELDS = ["ctx"]
974
975 def _is_simple_value(value):
976 """Is `value` simple enough to be displayed on a single line?"""
977 return (
978 value in [None, [], (), {}, set()] or
979 isinstance(value, (string_class, int, float))
980 )
981
982 # TODO: a test of ast_dump?
983 def ast_dump(node, depth=0):
984 """Dump the AST for `node`.
985
986 This recursively walks the AST, printing a readable version.
653 987
654 """ 988 """
655 def __init__(self, byte, line, first): 989 indent = " " * depth
656 self.byte = byte 990 if not isinstance(node, ast.AST):
657 self.line = line 991 print("{0}<{1} {2!r}>".format(indent, node.__class__.__name__, node))
658 self.first = first 992 return
659 self.length = 0 993
660 self.entrance = False 994 lineno = getattr(node, "lineno", None)
661 self.exits = set() 995 if lineno is not None:
662 996 linemark = " @ {0}".format(node.lineno)
663 def __repr__(self): 997 else:
664 return "<%d+%d @%d%s%s %r>" % ( 998 linemark = ""
665 self.byte, 999 head = "{0}<{1}{2}".format(indent, node.__class__.__name__, linemark)
666 self.length, 1000
667 self.line, 1001 named_fields = [
668 "!" if self.first else "", 1002 (name, value)
669 "v" if self.entrance else "", 1003 for name, value in ast.iter_fields(node)
670 list(self.exits), 1004 if name not in SKIP_DUMP_FIELDS
671 ) 1005 ]
1006 if not named_fields:
1007 print("{0}>".format(head))
1008 elif len(named_fields) == 1 and _is_simple_value(named_fields[0][1]):
1009 field_name, value = named_fields[0]
1010 print("{0} {1}: {2!r}>".format(head, field_name, value))
1011 else:
1012 print(head)
1013 if 0:
1014 print("{0}# mro: {1}".format(
1015 indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]),
1016 ))
1017 next_indent = indent + " "
1018 for field_name, value in named_fields:
1019 prefix = "{0}{1}:".format(next_indent, field_name)
1020 if _is_simple_value(value):
1021 print("{0} {1!r}".format(prefix, value))
1022 elif isinstance(value, list):
1023 print("{0} [".format(prefix))
1024 for n in value:
1025 ast_dump(n, depth + 8)
1026 print("{0}]".format(next_indent))
1027 else:
1028 print(prefix)
1029 ast_dump(value, depth + 8)
1030
1031 print("{0}>".format(indent))

eric ide

mercurial