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: |
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)) |