DebugClients/Python/coverage/parser.py

branch
debugger speed
changeset 5178
878ce843ca9f
parent 5051
3586ebd9fac8
child 6219
d6c795b5ce33
equal deleted inserted replaced
5174:8c48f5e0cd92 5178:878ce843ca9f
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
3
4 """Code parsing for coverage.py."""
5
6 import ast
7 import collections
8 import os
9 import re
10 import token
11 import tokenize
12
13 from coverage import env
14 from coverage.backward import range # pylint: disable=redefined-builtin
15 from coverage.backward import bytes_to_ints, string_class
16 from coverage.bytecode import CodeObjects
17 from coverage.debug import short_stack
18 from coverage.misc import contract, new_contract, nice_pair, join_regex
19 from coverage.misc import CoverageException, NoSource, NotPython
20 from coverage.phystokens import compile_unicode, generate_tokens, neuter_encoding_declaration
21
22
23 class PythonParser(object):
24 """Parse code to find executable lines, excluded lines, etc.
25
26 This information is all based on static analysis: no code execution is
27 involved.
28
29 """
30 @contract(text='unicode|None')
31 def __init__(self, text=None, filename=None, exclude=None):
32 """
33 Source can be provided as `text`, the text itself, or `filename`, from
34 which the text will be read. Excluded lines are those that match
35 `exclude`, a regex.
36
37 """
38 assert text or filename, "PythonParser needs either text or filename"
39 self.filename = filename or "<code>"
40 self.text = text
41 if not self.text:
42 from coverage.python import get_python_source
43 try:
44 self.text = get_python_source(self.filename)
45 except IOError as err:
46 raise NoSource(
47 "No source for code: '%s': %s" % (self.filename, err)
48 )
49
50 self.exclude = exclude
51
52 # The text lines of the parsed code.
53 self.lines = self.text.split('\n')
54
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.
62 self.excluded = set()
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
77 # The line numbers of docstring lines.
78 self.raw_docstrings = set()
79
80 # Internal detail, used by lab/parser.py.
81 self.show_tokens = False
82
83 # A dict mapping line numbers to lexical statement starts for
84 # multi-line statements.
85 self._multiline = {}
86
87 # Lazily-created ByteParser, arc data, and missing arc descriptions.
88 self._byte_parser = None
89 self._all_arcs = None
90 self._missing_arc_fragments = None
91
92 @property
93 def byte_parser(self):
94 """Create a ByteParser on demand."""
95 if not self._byte_parser:
96 self._byte_parser = ByteParser(self.text, filename=self.filename)
97 return self._byte_parser
98
99 def lines_matching(self, *regexes):
100 """Find the lines matching one of a list of regexes.
101
102 Returns a set of line numbers, the lines that contain a match for one
103 of the regexes in `regexes`. The entire line needn't match, just a
104 part of it.
105
106 """
107 combined = join_regex(regexes)
108 if env.PY2:
109 combined = combined.decode("utf8")
110 regex_c = re.compile(combined)
111 matches = set()
112 for i, ltext in enumerate(self.lines, start=1):
113 if regex_c.search(ltext):
114 matches.add(i)
115 return matches
116
117 def _raw_parse(self):
118 """Parse the source to find the interesting facts about its lines.
119
120 A handful of attributes are updated.
121
122 """
123 # Find lines which match an exclusion pattern.
124 if self.exclude:
125 self.raw_excluded = self.lines_matching(self.exclude)
126
127 # Tokenize, to find excluded suites, to find docstrings, and to find
128 # multi-line statements.
129 indent = 0
130 exclude_indent = 0
131 excluding = False
132 excluding_decorators = False
133 prev_toktype = token.INDENT
134 first_line = None
135 empty = True
136 first_on_line = True
137
138 tokgen = generate_tokens(self.text)
139 for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen:
140 if self.show_tokens: # pragma: not covered
141 print("%10s %5s %-20r %r" % (
142 tokenize.tok_name.get(toktype, toktype),
143 nice_pair((slineno, elineno)), ttext, ltext
144 ))
145 if toktype == token.INDENT:
146 indent += 1
147 elif toktype == token.DEDENT:
148 indent -= 1
149 elif toktype == token.NAME:
150 if ttext == 'class':
151 # Class definitions look like branches in the bytecode, so
152 # we need to exclude them. The simplest way is to note the
153 # lines with the 'class' keyword.
154 self.raw_classdefs.add(slineno)
155 elif toktype == token.OP:
156 if ttext == ':':
157 should_exclude = (elineno in self.raw_excluded) or excluding_decorators
158 if not excluding and should_exclude:
159 # Start excluding a suite. We trigger off of the colon
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)
172 elif toktype == token.STRING and prev_toktype == token.INDENT:
173 # Strings that are first on an indented line are docstrings.
174 # (a trick from trace.py in the stdlib.) This works for
175 # 99.9999% of cases. For the rest (!) see:
176 # http://stackoverflow.com/questions/1769332/x/1769794#1769794
177 self.raw_docstrings.update(range(slineno, elineno+1))
178 elif toktype == token.NEWLINE:
179 if first_line is not None and elineno != first_line:
180 # We're at the end of a line, and we've ended on a
181 # different line than the first line of the statement,
182 # so record a multi-line range.
183 for l in range(first_line, elineno+1):
184 self._multiline[l] = first_line
185 first_line = None
186 first_on_line = True
187
188 if ttext.strip() and toktype != tokenize.COMMENT:
189 # A non-whitespace token.
190 empty = False
191 if first_line is None:
192 # The token is not whitespace, and is the first in a
193 # statement.
194 first_line = slineno
195 # Check whether to end an excluded suite.
196 if excluding and indent <= exclude_indent:
197 excluding = False
198 if excluding:
199 self.raw_excluded.add(elineno)
200 first_on_line = False
201
202 prev_toktype = toktype
203
204 # Find the starts of the executable statements.
205 if not empty:
206 self.raw_statements.update(self.byte_parser._find_statements())
207
208 def first_line(self, line):
209 """Return the first line number of the statement including `line`."""
210 return self._multiline.get(line, line)
211
212 def first_lines(self, lines):
213 """Map the line numbers in `lines` to the correct first line of the
214 statement.
215
216 Returns a set of the first lines.
217
218 """
219 return set(self.first_line(l) for l in lines)
220
221 def translate_lines(self, lines):
222 """Implement `FileReporter.translate_lines`."""
223 return self.first_lines(lines)
224
225 def translate_arcs(self, arcs):
226 """Implement `FileReporter.translate_arcs`."""
227 return [(self.first_line(a), self.first_line(b)) for (a, b) in arcs]
228
229 def parse_source(self):
230 """Parse source text to find executable lines, excluded lines, etc.
231
232 Sets the .excluded and .statements attributes, normalized to the first
233 line of multi-line statements.
234
235 """
236 try:
237 self._raw_parse()
238 except (tokenize.TokenError, IndentationError) as err:
239 if hasattr(err, "lineno"):
240 lineno = err.lineno # IndentationError
241 else:
242 lineno = err.args[1][0] # TokenError
243 raise NotPython(
244 u"Couldn't parse '%s' as Python source: '%s' at line %d" % (
245 self.filename, err.args[0], lineno
246 )
247 )
248
249 self.excluded = self.first_lines(self.raw_excluded)
250
251 ignore = self.excluded | self.raw_docstrings
252 starts = self.raw_statements - ignore
253 self.statements = self.first_lines(starts) - ignore
254
255 def arcs(self):
256 """Get information about the arcs available in the code.
257
258 Returns a set of line number pairs. Line numbers have been normalized
259 to the first line of multi-line statements.
260
261 """
262 if self._all_arcs is None:
263 self._analyze_ast()
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
283
284 def exit_counts(self):
285 """Get a count of exits from that each line.
286
287 Excluded lines are excluded.
288
289 """
290 exit_counts = collections.defaultdict(int)
291 for l1, l2 in self.arcs():
292 if l1 < 0:
293 # Don't ever report -1 as a line number
294 continue
295 if l1 in self.excluded:
296 # Don't report excluded lines as line numbers.
297 continue
298 if l2 in self.excluded:
299 # Arcs to excluded lines shouldn't count.
300 continue
301 exit_counts[l1] += 1
302
303 # Class definitions have one extra exit, so remove one for each:
304 for l in self.raw_classdefs:
305 # Ensure key is there: class definitions can include excluded lines.
306 if l in exit_counts:
307 exit_counts[l] -= 1
308
309 return exit_counts
310
311 def missing_arc_description(self, start, end, executed_arcs=None):
312 """Provide an English sentence describing a missing arc."""
313 if self._missing_arc_fragments is None:
314 self._analyze_ast()
315
316 actual_start = start
317
318 if (
319 executed_arcs and
320 end < 0 and end == -start and
321 (end, start) not in executed_arcs and
322 (end, start) in self._missing_arc_fragments
323 ):
324 # It's a one-line callable, and we never even started it,
325 # and we have a message about not starting it.
326 start, end = end, start
327
328 fragment_pairs = self._missing_arc_fragments.get((start, end), [(None, None)])
329
330 msgs = []
331 for fragment_pair in fragment_pairs:
332 smsg, emsg = fragment_pair
333
334 if emsg is None:
335 if end < 0:
336 # Hmm, maybe we have a one-line callable, let's check.
337 if (-end, end) in self._missing_arc_fragments:
338 return self.missing_arc_description(-end, end)
339 emsg = "didn't jump to the function exit"
340 else:
341 emsg = "didn't jump to line {lineno}"
342 emsg = emsg.format(lineno=end)
343
344 msg = "line {start} {emsg}".format(start=actual_start, emsg=emsg)
345 if smsg is not None:
346 msg += ", because {smsg}".format(smsg=smsg.format(lineno=actual_start))
347
348 msgs.append(msg)
349
350 return " or ".join(msgs)
351
352
353 class ByteParser(object):
354 """Parse bytecode to understand the structure of code."""
355
356 @contract(text='unicode')
357 def __init__(self, text, code=None, filename=None):
358 self.text = text
359 if code:
360 self.code = code
361 else:
362 try:
363 self.code = compile_unicode(text, filename, "exec")
364 except SyntaxError as synerr:
365 raise NotPython(
366 u"Couldn't parse '%s' as Python source: '%s' at line %d" % (
367 filename, synerr.msg, synerr.lineno
368 )
369 )
370
371 # Alternative Python implementations don't always provide all the
372 # attributes on code objects that we need to do the analysis.
373 for attr in ['co_lnotab', 'co_firstlineno', 'co_consts']:
374 if not hasattr(self.code, attr):
375 raise CoverageException(
376 "This implementation of Python doesn't support code analysis.\n"
377 "Run coverage.py under CPython for this command."
378 )
379
380 def child_parsers(self):
381 """Iterate over all the code objects nested within this one.
382
383 The iteration includes `self` as its first value.
384
385 """
386 children = CodeObjects(self.code)
387 return (ByteParser(self.text, code=c) for c in children)
388
389 def _bytes_lines(self):
390 """Map byte offsets to line numbers in `code`.
391
392 Uses co_lnotab described in Python/compile.c to map byte offsets to
393 line numbers. Produces a sequence: (b0, l0), (b1, l1), ...
394
395 Only byte offsets that correspond to line numbers are included in the
396 results.
397
398 """
399 # Adapted from dis.py in the standard library.
400 byte_increments = bytes_to_ints(self.code.co_lnotab[0::2])
401 line_increments = bytes_to_ints(self.code.co_lnotab[1::2])
402
403 last_line_num = None
404 line_num = self.code.co_firstlineno
405 byte_num = 0
406 for byte_incr, line_incr in zip(byte_increments, line_increments):
407 if byte_incr:
408 if line_num != last_line_num:
409 yield (byte_num, line_num)
410 last_line_num = line_num
411 byte_num += byte_incr
412 line_num += line_incr
413 if line_num != last_line_num:
414 yield (byte_num, line_num)
415
416 def _find_statements(self):
417 """Find the statements in `self.code`.
418
419 Produce a sequence of line numbers that start statements. Recurses
420 into all code objects reachable from `self.code`.
421
422 """
423 for bp in self.child_parsers():
424 # Get all of the lineno information from this code.
425 for _, l in bp._bytes_lines():
426 yield l
427
428
429 #
430 # AST analysis
431 #
432
433 class LoopBlock(object):
434 """A block on the block stack representing a `for` or `while` loop."""
435 def __init__(self, start):
436 self.start = start
437 self.break_exits = set()
438
439
440 class FunctionBlock(object):
441 """A block on the block stack representing a function definition."""
442 def __init__(self, start, name):
443 self.start = start
444 self.name = name
445
446
447 class TryBlock(object):
448 """A block on the block stack representing a `try` block."""
449 def __init__(self, handler_start=None, final_start=None):
450 self.handler_start = handler_start
451 self.final_start = final_start
452 self.break_from = set()
453 self.continue_from = set()
454 self.return_from = set()
455 self.raise_from = set()
456
457
458 class ArcStart(collections.namedtuple("Arc", "lineno, cause")):
459 """The information needed to start an arc.
460
461 `lineno` is the line number the arc starts from. `cause` is a fragment
462 used as the startmsg for AstArcAnalyzer.missing_arc_fragments.
463
464 """
465 def __new__(cls, lineno, cause=None):
466 return super(ArcStart, cls).__new__(cls, lineno, cause)
467
468
469 # Define contract words that PyContract doesn't have.
470 # ArcStarts is for a list or set of ArcStart's.
471 new_contract('ArcStarts', lambda seq: all(isinstance(x, ArcStart) for x in seq))
472
473
474 class AstArcAnalyzer(object):
475 """Analyze source text with an AST to find executable code paths."""
476
477 @contract(text='unicode', statements=set)
478 def __init__(self, text, statements, multiline):
479 self.root_node = ast.parse(neuter_encoding_declaration(text))
480 # TODO: I think this is happening in too many places.
481 self.statements = set(multiline.get(l, l) for l in statements)
482 self.multiline = multiline
483
484 if int(os.environ.get("COVERAGE_ASTDUMP", 0)): # pragma: debugging
485 # Dump the AST so that failing tests have helpful output.
486 print("Statements: {}".format(self.statements))
487 print("Multiline map: {}".format(self.multiline))
488 ast_dump(self.root_node)
489
490 self.arcs = set()
491
492 # A map from arc pairs to a pair of sentence fragments: (startmsg, endmsg).
493 # For an arc from line 17, they should be usable like:
494 # "Line 17 {endmsg}, because {startmsg}"
495 self.missing_arc_fragments = collections.defaultdict(list)
496 self.block_stack = []
497
498 self.debug = bool(int(os.environ.get("COVERAGE_TRACK_ARCS", 0)))
499
500 def analyze(self):
501 """Examine the AST tree from `root_node` to determine possible arcs.
502
503 This sets the `arcs` attribute to be a set of (from, to) line number
504 pairs.
505
506 """
507 for node in ast.walk(self.root_node):
508 node_name = node.__class__.__name__
509 code_object_handler = getattr(self, "_code_object__" + node_name, None)
510 if code_object_handler is not None:
511 code_object_handler(node)
512
513 def add_arc(self, start, end, smsg=None, emsg=None):
514 """Add an arc, including message fragments to use if it is missing."""
515 if self.debug:
516 print("\nAdding arc: ({}, {}): {!r}, {!r}".format(start, end, smsg, emsg))
517 print(short_stack(limit=6))
518 self.arcs.add((start, end))
519
520 if smsg is not None or emsg is not None:
521 self.missing_arc_fragments[(start, end)].append((smsg, emsg))
522
523 def nearest_blocks(self):
524 """Yield the blocks in nearest-to-farthest order."""
525 return reversed(self.block_stack)
526
527 @contract(returns=int)
528 def line_for_node(self, node):
529 """What is the right line number to use for this node?
530
531 This dispatches to _line__Node functions where needed.
532
533 """
534 node_name = node.__class__.__name__
535 handler = getattr(self, "_line__" + node_name, None)
536 if handler is not None:
537 return handler(node)
538 else:
539 return node.lineno
540
541 def _line__Assign(self, node):
542 return self.line_for_node(node.value)
543
544 def _line__Dict(self, node):
545 # Python 3.5 changed how dict literals are made.
546 if env.PYVERSION >= (3, 5) and node.keys:
547 if node.keys[0] is not None:
548 return node.keys[0].lineno
549 else:
550 # Unpacked dict literals `{**{'a':1}}` have None as the key,
551 # use the value in that case.
552 return node.values[0].lineno
553 else:
554 return node.lineno
555
556 def _line__List(self, node):
557 if node.elts:
558 return self.line_for_node(node.elts[0])
559 else:
560 return node.lineno
561
562 def _line__Module(self, node):
563 if node.body:
564 return self.line_for_node(node.body[0])
565 else:
566 # Modules have no line number, they always start at 1.
567 return 1
568
569 OK_TO_DEFAULT = set([
570 "Assign", "Assert", "AugAssign", "Delete", "Exec", "Expr", "Global",
571 "Import", "ImportFrom", "Nonlocal", "Pass", "Print",
572 ])
573
574 @contract(returns='ArcStarts')
575 def add_arcs(self, node):
576 """Add the arcs for `node`.
577
578 Return a set of ArcStarts, exits from this node to the next.
579
580 """
581 node_name = node.__class__.__name__
582 handler = getattr(self, "_handle__" + node_name, None)
583 if handler is not None:
584 return handler(node)
585
586 if 0:
587 node_name = node.__class__.__name__
588 if node_name not in self.OK_TO_DEFAULT:
589 print("*** Unhandled: {0}".format(node))
590 return set([ArcStart(self.line_for_node(node), cause=None)])
591
592 @contract(returns='ArcStarts')
593 def add_body_arcs(self, body, from_start=None, prev_starts=None):
594 """Add arcs for the body of a compound statement.
595
596 `body` is the body node. `from_start` is a single `ArcStart` that can
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:
610 continue
611 for prev_start in prev_starts:
612 self.add_arc(prev_start.lineno, lineno, prev_start.cause)
613 prev_starts = self.add_arcs(body_node)
614 return prev_starts
615
616 def is_constant_expr(self, node):
617 """Is this a compile-time constant?"""
618 node_name = node.__class__.__name__
619 if node_name in ["NameConstant", "Num"]:
620 return True
621 elif node_name == "Name":
622 if env.PY3 and node.id in ["True", "False", "None"]:
623 return True
624 return False
625
626 # tests to write:
627 # TODO: while EXPR:
628 # TODO: while False:
629 # TODO: listcomps hidden deep in other expressions
630 # TODO: listcomps hidden in lists: x = [[i for i in range(10)]]
631 # TODO: nested function definitions
632
633 @contract(exits='ArcStarts')
634 def process_break_exits(self, exits):
635 """Add arcs due to jumps from `exits` being breaks."""
636 for block in self.nearest_blocks():
637 if isinstance(block, LoopBlock):
638 block.break_exits.update(exits)
639 break
640 elif isinstance(block, TryBlock) and block.final_start is not None:
641 block.break_from.update(exits)
642 break
643
644 @contract(exits='ArcStarts')
645 def process_continue_exits(self, exits):
646 """Add arcs due to jumps from `exits` being continues."""
647 for block in self.nearest_blocks():
648 if isinstance(block, LoopBlock):
649 for xit in exits:
650 self.add_arc(xit.lineno, block.start, xit.cause)
651 break
652 elif isinstance(block, TryBlock) and block.final_start is not None:
653 block.continue_from.update(exits)
654 break
655
656 @contract(exits='ArcStarts')
657 def process_raise_exits(self, exits):
658 """Add arcs due to jumps from `exits` being raises."""
659 for block in self.nearest_blocks():
660 if isinstance(block, TryBlock):
661 if block.handler_start is not None:
662 for xit in exits:
663 self.add_arc(xit.lineno, block.handler_start, xit.cause)
664 break
665 elif block.final_start is not None:
666 block.raise_from.update(exits)
667 break
668 elif isinstance(block, FunctionBlock):
669 for xit in exits:
670 self.add_arc(
671 xit.lineno, -block.start, xit.cause,
672 "didn't except from function '{0}'".format(block.name),
673 )
674 break
675
676 @contract(exits='ArcStarts')
677 def process_return_exits(self, exits):
678 """Add arcs due to jumps from `exits` being returns."""
679 for block in self.nearest_blocks():
680 if isinstance(block, TryBlock) and block.final_start is not None:
681 block.return_from.update(exits)
682 break
683 elif isinstance(block, FunctionBlock):
684 for xit in exits:
685 self.add_arc(
686 xit.lineno, -block.start, xit.cause,
687 "didn't return from function '{0}'".format(block.name),
688 )
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.
987
988 """
989 indent = " " * depth
990 if not isinstance(node, ast.AST):
991 print("{0}<{1} {2!r}>".format(indent, node.__class__.__name__, node))
992 return
993
994 lineno = getattr(node, "lineno", None)
995 if lineno is not None:
996 linemark = " @ {0}".format(node.lineno)
997 else:
998 linemark = ""
999 head = "{0}<{1}{2}".format(indent, node.__class__.__name__, linemark)
1000
1001 named_fields = [
1002 (name, value)
1003 for name, value in ast.iter_fields(node)
1004 if name not in SKIP_DUMP_FIELDS
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