coverage 7.6.10__cp312-cp312-musllinux_1_2_aarch64.whl → 7.12.0__cp312-cp312-musllinux_1_2_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coverage/__init__.py +3 -1
- coverage/__main__.py +3 -1
- coverage/annotate.py +2 -3
- coverage/bytecode.py +178 -4
- coverage/cmdline.py +330 -155
- coverage/collector.py +32 -43
- coverage/config.py +167 -63
- coverage/context.py +5 -6
- coverage/control.py +165 -86
- coverage/core.py +71 -34
- coverage/data.py +4 -5
- coverage/debug.py +113 -57
- coverage/disposition.py +2 -1
- coverage/env.py +29 -78
- coverage/exceptions.py +29 -7
- coverage/execfile.py +19 -14
- coverage/files.py +24 -19
- coverage/html.py +118 -75
- coverage/htmlfiles/coverage_html.js +12 -10
- coverage/htmlfiles/index.html +45 -10
- coverage/htmlfiles/pyfile.html +2 -2
- coverage/htmlfiles/style.css +54 -6
- coverage/htmlfiles/style.scss +85 -3
- coverage/inorout.py +62 -45
- coverage/jsonreport.py +22 -9
- coverage/lcovreport.py +16 -18
- coverage/misc.py +51 -47
- coverage/multiproc.py +12 -7
- coverage/numbits.py +4 -5
- coverage/parser.py +150 -251
- coverage/patch.py +166 -0
- coverage/phystokens.py +25 -26
- coverage/plugin.py +14 -14
- coverage/plugin_support.py +37 -36
- coverage/python.py +13 -14
- coverage/pytracer.py +31 -33
- coverage/regions.py +3 -2
- coverage/report.py +60 -44
- coverage/report_core.py +7 -10
- coverage/results.py +152 -68
- coverage/sqldata.py +261 -211
- coverage/sqlitedb.py +37 -29
- coverage/sysmon.py +237 -162
- coverage/templite.py +19 -7
- coverage/tomlconfig.py +13 -13
- coverage/tracer.cpython-312-aarch64-linux-musl.so +0 -0
- coverage/tracer.pyi +3 -1
- coverage/types.py +26 -23
- coverage/version.py +4 -19
- coverage/xmlreport.py +17 -14
- {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/METADATA +50 -28
- coverage-7.12.0.dist-info/RECORD +59 -0
- {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/WHEEL +1 -1
- coverage-7.6.10.dist-info/RECORD +0 -58
- {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/entry_points.txt +0 -0
- {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info/licenses}/LICENSE.txt +0 -0
- {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/top_level.txt +0 -0
coverage/parser.py
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
|
2
|
-
# For details: https://github.com/
|
|
2
|
+
# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
|
|
3
3
|
|
|
4
4
|
"""Code parsing for coverage.py."""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import ast
|
|
9
|
-
import functools
|
|
10
9
|
import collections
|
|
10
|
+
import functools
|
|
11
11
|
import os
|
|
12
12
|
import re
|
|
13
|
-
import sys
|
|
14
13
|
import token
|
|
15
14
|
import tokenize
|
|
16
|
-
|
|
17
15
|
from collections.abc import Iterable, Sequence
|
|
18
16
|
from dataclasses import dataclass
|
|
19
17
|
from types import CodeType
|
|
20
|
-
from typing import
|
|
18
|
+
from typing import Callable, Optional, Protocol, cast
|
|
21
19
|
|
|
22
20
|
from coverage import env
|
|
23
21
|
from coverage.bytecode import code_objects
|
|
@@ -37,6 +35,7 @@ class PythonParser:
|
|
|
37
35
|
involved.
|
|
38
36
|
|
|
39
37
|
"""
|
|
38
|
+
|
|
40
39
|
def __init__(
|
|
41
40
|
self,
|
|
42
41
|
text: str | None = None,
|
|
@@ -55,6 +54,7 @@ class PythonParser:
|
|
|
55
54
|
self.text: str = text
|
|
56
55
|
else:
|
|
57
56
|
from coverage.python import get_python_source
|
|
57
|
+
|
|
58
58
|
try:
|
|
59
59
|
self.text = get_python_source(self.filename)
|
|
60
60
|
except OSError as err:
|
|
@@ -92,7 +92,7 @@ class PythonParser:
|
|
|
92
92
|
|
|
93
93
|
# A dict mapping line numbers to lexical statement starts for
|
|
94
94
|
# multi-line statements.
|
|
95
|
-
self.
|
|
95
|
+
self.multiline_map: dict[TLineNo, TLineNo] = {}
|
|
96
96
|
|
|
97
97
|
# Lazily-created arc data, and missing arc descriptions.
|
|
98
98
|
self._all_arcs: set[TArc] | None = None
|
|
@@ -113,9 +113,11 @@ class PythonParser:
|
|
|
113
113
|
last_start_line = 0
|
|
114
114
|
for match in re.finditer(regex, self.text, flags=re.MULTILINE):
|
|
115
115
|
start, end = match.span()
|
|
116
|
-
start_line = last_start_line + self.text.count(
|
|
117
|
-
end_line = last_start_line + self.text.count(
|
|
118
|
-
matches.update(
|
|
116
|
+
start_line = last_start_line + self.text.count("\n", last_start, start)
|
|
117
|
+
end_line = last_start_line + self.text.count("\n", last_start, end)
|
|
118
|
+
matches.update(
|
|
119
|
+
self.multiline_map.get(i, i) for i in range(start_line + 1, end_line + 2)
|
|
120
|
+
)
|
|
119
121
|
last_start = start
|
|
120
122
|
last_start_line = start_line
|
|
121
123
|
return matches
|
|
@@ -147,20 +149,23 @@ class PythonParser:
|
|
|
147
149
|
assert self.text is not None
|
|
148
150
|
tokgen = generate_tokens(self.text)
|
|
149
151
|
for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen:
|
|
150
|
-
if self.show_tokens:
|
|
151
|
-
print(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
if self.show_tokens: # pragma: debugging
|
|
153
|
+
print(
|
|
154
|
+
"%10s %5s %-20r %r"
|
|
155
|
+
% (
|
|
156
|
+
tokenize.tok_name.get(toktype, toktype),
|
|
157
|
+
nice_pair((slineno, elineno)),
|
|
158
|
+
ttext,
|
|
159
|
+
ltext,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
155
162
|
if toktype == token.INDENT:
|
|
156
163
|
indent += 1
|
|
157
164
|
elif toktype == token.DEDENT:
|
|
158
165
|
indent -= 1
|
|
159
166
|
elif toktype == token.OP:
|
|
160
167
|
if ttext == ":" and nesting == 0:
|
|
161
|
-
should_exclude = (
|
|
162
|
-
self.excluded.intersection(range(first_line, elineno + 1))
|
|
163
|
-
)
|
|
168
|
+
should_exclude = self.excluded.intersection(range(first_line, elineno + 1))
|
|
164
169
|
if not excluding and should_exclude:
|
|
165
170
|
# Start excluding a suite. We trigger off of the colon
|
|
166
171
|
# token so that the #pragma comment will be recognized on
|
|
@@ -177,8 +182,8 @@ class PythonParser:
|
|
|
177
182
|
# We're at the end of a line, and we've ended on a
|
|
178
183
|
# different line than the first line of the statement,
|
|
179
184
|
# so record a multi-line range.
|
|
180
|
-
for l in range(first_line, elineno+1):
|
|
181
|
-
self.
|
|
185
|
+
for l in range(first_line, elineno + 1):
|
|
186
|
+
self.multiline_map[l] = first_line
|
|
182
187
|
first_line = 0
|
|
183
188
|
|
|
184
189
|
if ttext.strip() and toktype != tokenize.COMMENT:
|
|
@@ -198,12 +203,6 @@ class PythonParser:
|
|
|
198
203
|
byte_parser = ByteParser(self.text, filename=self.filename)
|
|
199
204
|
self.raw_statements.update(byte_parser._find_statements())
|
|
200
205
|
|
|
201
|
-
# The first line of modules can lie and say 1 always, even if the first
|
|
202
|
-
# line of code is later. If so, map 1 to the actual first line of the
|
|
203
|
-
# module.
|
|
204
|
-
if env.PYBEHAVIOR.module_firstline_1 and self._multiline:
|
|
205
|
-
self._multiline[1] = min(self.raw_statements)
|
|
206
|
-
|
|
207
206
|
self.excluded = self.first_lines(self.excluded)
|
|
208
207
|
|
|
209
208
|
# AST lets us find classes, docstrings, and decorator-affected
|
|
@@ -233,9 +232,9 @@ class PythonParser:
|
|
|
233
232
|
def first_line(self, lineno: TLineNo) -> TLineNo:
|
|
234
233
|
"""Return the first line number of the statement including `lineno`."""
|
|
235
234
|
if lineno < 0:
|
|
236
|
-
lineno = -self.
|
|
235
|
+
lineno = -self.multiline_map.get(-lineno, -lineno)
|
|
237
236
|
else:
|
|
238
|
-
lineno = self.
|
|
237
|
+
lineno = self.multiline_map.get(lineno, lineno)
|
|
239
238
|
return lineno
|
|
240
239
|
|
|
241
240
|
def first_lines(self, linenos: Iterable[TLineNo]) -> set[TLineNo]:
|
|
@@ -267,12 +266,12 @@ class PythonParser:
|
|
|
267
266
|
self._raw_parse()
|
|
268
267
|
except (tokenize.TokenError, IndentationError, SyntaxError) as err:
|
|
269
268
|
if hasattr(err, "lineno"):
|
|
270
|
-
lineno = err.lineno
|
|
269
|
+
lineno = err.lineno # IndentationError
|
|
271
270
|
else:
|
|
272
|
-
lineno = err.args[1][0]
|
|
271
|
+
lineno = err.args[1][0] # TokenError
|
|
273
272
|
raise NotPython(
|
|
274
|
-
f"Couldn't parse '{self.filename}' as Python source: "
|
|
275
|
-
f"{err.args[0]!r} at line {lineno}",
|
|
273
|
+
f"Couldn't parse '{self.filename}' as Python source: "
|
|
274
|
+
+ f"{err.args[0]!r} at line {lineno}",
|
|
276
275
|
) from err
|
|
277
276
|
|
|
278
277
|
ignore = self.excluded | self.raw_docstrings
|
|
@@ -298,13 +297,12 @@ class PythonParser:
|
|
|
298
297
|
|
|
299
298
|
"""
|
|
300
299
|
assert self._ast_root is not None
|
|
301
|
-
aaa = AstArcAnalyzer(self.filename, self._ast_root, self.raw_statements, self.
|
|
300
|
+
aaa = AstArcAnalyzer(self.filename, self._ast_root, self.raw_statements, self.multiline_map)
|
|
302
301
|
aaa.analyze()
|
|
303
302
|
arcs = aaa.arcs
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
arcs = self.fix_with_jumps(arcs)
|
|
303
|
+
self._with_jump_fixers = aaa.with_jump_fixers()
|
|
304
|
+
if self._with_jump_fixers:
|
|
305
|
+
arcs = self.fix_with_jumps(arcs)
|
|
308
306
|
|
|
309
307
|
self._all_arcs = set()
|
|
310
308
|
for l1, l2 in arcs:
|
|
@@ -453,33 +451,11 @@ class ByteParser:
|
|
|
453
451
|
def _line_numbers(self) -> Iterable[TLineNo]:
|
|
454
452
|
"""Yield the line numbers possible in this code object.
|
|
455
453
|
|
|
456
|
-
Uses
|
|
457
|
-
line numbers. Produces a sequence: l0, l1, ...
|
|
454
|
+
Uses co_lines() to produce a sequence: l0, l1, ...
|
|
458
455
|
"""
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if line:
|
|
463
|
-
yield line
|
|
464
|
-
else:
|
|
465
|
-
# Adapted from dis.py in the standard library.
|
|
466
|
-
byte_increments = self.code.co_lnotab[0::2]
|
|
467
|
-
line_increments = self.code.co_lnotab[1::2]
|
|
468
|
-
|
|
469
|
-
last_line_num = None
|
|
470
|
-
line_num = self.code.co_firstlineno
|
|
471
|
-
byte_num = 0
|
|
472
|
-
for byte_incr, line_incr in zip(byte_increments, line_increments):
|
|
473
|
-
if byte_incr:
|
|
474
|
-
if line_num != last_line_num:
|
|
475
|
-
yield line_num
|
|
476
|
-
last_line_num = line_num
|
|
477
|
-
byte_num += byte_incr
|
|
478
|
-
if line_incr >= 0x80:
|
|
479
|
-
line_incr -= 0x100
|
|
480
|
-
line_num += line_incr
|
|
481
|
-
if line_num != last_line_num:
|
|
482
|
-
yield line_num
|
|
456
|
+
for _, _, line in self.code.co_lines():
|
|
457
|
+
if line:
|
|
458
|
+
yield line
|
|
483
459
|
|
|
484
460
|
def _find_statements(self) -> Iterable[TLineNo]:
|
|
485
461
|
"""Find the statements in `self.code`.
|
|
@@ -497,6 +473,7 @@ class ByteParser:
|
|
|
497
473
|
# AST analysis
|
|
498
474
|
#
|
|
499
475
|
|
|
476
|
+
|
|
500
477
|
@dataclass(frozen=True, order=True)
|
|
501
478
|
class ArcStart:
|
|
502
479
|
"""The information needed to start an arc.
|
|
@@ -527,12 +504,14 @@ class ArcStart:
|
|
|
527
504
|
"line 1 didn't jump to line 2 because the condition on line 1 was never true."
|
|
528
505
|
|
|
529
506
|
"""
|
|
507
|
+
|
|
530
508
|
lineno: TLineNo
|
|
531
509
|
cause: str = ""
|
|
532
510
|
|
|
533
511
|
|
|
534
512
|
class TAddArcFn(Protocol):
|
|
535
513
|
"""The type for AstArcAnalyzer.add_arc()."""
|
|
514
|
+
|
|
536
515
|
def __call__(
|
|
537
516
|
self,
|
|
538
517
|
start: TLineNo,
|
|
@@ -555,6 +534,7 @@ class TAddArcFn(Protocol):
|
|
|
555
534
|
|
|
556
535
|
TArcFragments = dict[TArc, list[tuple[Optional[str], Optional[str]]]]
|
|
557
536
|
|
|
537
|
+
|
|
558
538
|
class Block:
|
|
559
539
|
"""
|
|
560
540
|
Blocks need to handle various exiting statements in their own ways.
|
|
@@ -564,6 +544,7 @@ class Block:
|
|
|
564
544
|
exits are handled, or False if the search should continue up the block
|
|
565
545
|
stack.
|
|
566
546
|
"""
|
|
547
|
+
|
|
567
548
|
# pylint: disable=unused-argument
|
|
568
549
|
def process_break_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool:
|
|
569
550
|
"""Process break exits."""
|
|
@@ -584,6 +565,7 @@ class Block:
|
|
|
584
565
|
|
|
585
566
|
class LoopBlock(Block):
|
|
586
567
|
"""A block on the block stack representing a `for` or `while` loop."""
|
|
568
|
+
|
|
587
569
|
def __init__(self, start: TLineNo) -> None:
|
|
588
570
|
# The line number where the loop starts.
|
|
589
571
|
self.start = start
|
|
@@ -602,6 +584,7 @@ class LoopBlock(Block):
|
|
|
602
584
|
|
|
603
585
|
class FunctionBlock(Block):
|
|
604
586
|
"""A block on the block stack representing a function definition."""
|
|
587
|
+
|
|
605
588
|
def __init__(self, start: TLineNo, name: str) -> None:
|
|
606
589
|
# The line number where the function starts.
|
|
607
590
|
self.start = start
|
|
@@ -611,7 +594,9 @@ class FunctionBlock(Block):
|
|
|
611
594
|
def process_raise_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool:
|
|
612
595
|
for xit in exits:
|
|
613
596
|
add_arc(
|
|
614
|
-
xit.lineno,
|
|
597
|
+
xit.lineno,
|
|
598
|
+
-self.start,
|
|
599
|
+
xit.cause,
|
|
615
600
|
f"except from function {self.name!r}",
|
|
616
601
|
)
|
|
617
602
|
return True
|
|
@@ -619,7 +604,9 @@ class FunctionBlock(Block):
|
|
|
619
604
|
def process_return_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool:
|
|
620
605
|
for xit in exits:
|
|
621
606
|
add_arc(
|
|
622
|
-
xit.lineno,
|
|
607
|
+
xit.lineno,
|
|
608
|
+
-self.start,
|
|
609
|
+
xit.cause,
|
|
623
610
|
f"return from function {self.name!r}",
|
|
624
611
|
)
|
|
625
612
|
return True
|
|
@@ -627,6 +614,7 @@ class FunctionBlock(Block):
|
|
|
627
614
|
|
|
628
615
|
class TryBlock(Block):
|
|
629
616
|
"""A block on the block stack representing a `try` block."""
|
|
617
|
+
|
|
630
618
|
def __init__(self, handler_start: TLineNo | None, final_start: TLineNo | None) -> None:
|
|
631
619
|
# The line number of the first "except" handler, if any.
|
|
632
620
|
self.handler_start = handler_start
|
|
@@ -640,18 +628,33 @@ class TryBlock(Block):
|
|
|
640
628
|
return True
|
|
641
629
|
|
|
642
630
|
|
|
643
|
-
|
|
644
|
-
"""A synthetic fictitious node, containing a sequence of nodes.
|
|
631
|
+
# TODO: Shouldn't the cause messages join with "and" instead of "or"?
|
|
645
632
|
|
|
646
|
-
This is used when collapsing optimized if-statements, to represent the
|
|
647
|
-
unconditional execution of one of the clauses.
|
|
648
633
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
self.body = body
|
|
652
|
-
self.lineno = body[0].lineno # type: ignore[attr-defined]
|
|
634
|
+
def is_constant_test_expr(node: ast.AST) -> tuple[bool, bool]:
|
|
635
|
+
"""Is this a compile-time constant test expression?
|
|
653
636
|
|
|
654
|
-
|
|
637
|
+
We don't try to mimic all of CPython's optimizations. We just have to
|
|
638
|
+
handle the kinds of constant expressions people might actually use.
|
|
639
|
+
|
|
640
|
+
"""
|
|
641
|
+
match node:
|
|
642
|
+
case ast.Constant():
|
|
643
|
+
return True, bool(node.value)
|
|
644
|
+
case ast.Name():
|
|
645
|
+
if node.id in ["True", "False", "None", "__debug__"]:
|
|
646
|
+
return True, eval(node.id) # pylint: disable=eval-used
|
|
647
|
+
case ast.UnaryOp():
|
|
648
|
+
if isinstance(node.op, ast.Not):
|
|
649
|
+
is_constant, val = is_constant_test_expr(node.operand)
|
|
650
|
+
return is_constant, not val
|
|
651
|
+
case ast.BoolOp():
|
|
652
|
+
rets = [is_constant_test_expr(v) for v in node.values]
|
|
653
|
+
is_constant = all(is_const for is_const, _ in rets)
|
|
654
|
+
if is_constant:
|
|
655
|
+
op = any if isinstance(node.op, ast.Or) else all
|
|
656
|
+
return True, op(v for _, v in rets)
|
|
657
|
+
return False, False
|
|
655
658
|
|
|
656
659
|
|
|
657
660
|
class AstArcAnalyzer:
|
|
@@ -691,7 +694,7 @@ class AstArcAnalyzer:
|
|
|
691
694
|
# $set_env.py: COVERAGE_AST_DUMP - Dump the AST nodes when parsing code.
|
|
692
695
|
dump_ast = bool(int(os.getenv("COVERAGE_AST_DUMP", "0")))
|
|
693
696
|
|
|
694
|
-
if dump_ast:
|
|
697
|
+
if dump_ast: # pragma: debugging
|
|
695
698
|
# Dump the AST so that failing tests have helpful output.
|
|
696
699
|
print(f"Statements: {self.statements}")
|
|
697
700
|
print(f"Multiline map: {self.multiline}")
|
|
@@ -717,7 +720,7 @@ class AstArcAnalyzer:
|
|
|
717
720
|
"""Examine the AST tree from `self.root_node` to determine possible arcs."""
|
|
718
721
|
for node in ast.walk(self.root_node):
|
|
719
722
|
node_name = node.__class__.__name__
|
|
720
|
-
code_object_handler = getattr(self, "_code_object__"
|
|
723
|
+
code_object_handler = getattr(self, f"_code_object__{node_name}", None)
|
|
721
724
|
if code_object_handler is not None:
|
|
722
725
|
code_object_handler(node)
|
|
723
726
|
|
|
@@ -785,7 +788,7 @@ class AstArcAnalyzer:
|
|
|
785
788
|
action_msg: str | None = None,
|
|
786
789
|
) -> None:
|
|
787
790
|
"""Add an arc, including message fragments to use if it is missing."""
|
|
788
|
-
if self.debug:
|
|
791
|
+
if self.debug: # pragma: debugging
|
|
789
792
|
print(f"Adding possible arc: ({start}, {end}): {missing_cause_msg!r}, {action_msg!r}")
|
|
790
793
|
print(short_stack(), end="\n\n")
|
|
791
794
|
self.arcs.add((start, end))
|
|
@@ -808,12 +811,12 @@ class AstArcAnalyzer:
|
|
|
808
811
|
node_name = node.__class__.__name__
|
|
809
812
|
handler = cast(
|
|
810
813
|
Optional[Callable[[ast.AST], TLineNo]],
|
|
811
|
-
getattr(self, "_line__"
|
|
814
|
+
getattr(self, f"_line__{node_name}", None),
|
|
812
815
|
)
|
|
813
816
|
if handler is not None:
|
|
814
817
|
line = handler(node)
|
|
815
818
|
else:
|
|
816
|
-
line = node.lineno
|
|
819
|
+
line = node.lineno # type: ignore[attr-defined]
|
|
817
820
|
return self.multiline.get(line, line)
|
|
818
821
|
|
|
819
822
|
# First lines: _line__*
|
|
@@ -854,19 +857,22 @@ class AstArcAnalyzer:
|
|
|
854
857
|
else:
|
|
855
858
|
return node.lineno
|
|
856
859
|
|
|
857
|
-
def _line__Module(self, node: ast.Module) -> TLineNo:
|
|
858
|
-
|
|
859
|
-
return 1
|
|
860
|
-
elif node.body:
|
|
861
|
-
return self.line_for_node(node.body[0])
|
|
862
|
-
else:
|
|
863
|
-
# Empty modules have no line number, they always start at 1.
|
|
864
|
-
return 1
|
|
860
|
+
def _line__Module(self, node: ast.Module) -> TLineNo: # pylint: disable=unused-argument
|
|
861
|
+
return 1
|
|
865
862
|
|
|
866
863
|
# The node types that just flow to the next node with no complications.
|
|
867
864
|
OK_TO_DEFAULT = {
|
|
868
|
-
"AnnAssign",
|
|
869
|
-
"
|
|
865
|
+
"AnnAssign",
|
|
866
|
+
"Assign",
|
|
867
|
+
"Assert",
|
|
868
|
+
"AugAssign",
|
|
869
|
+
"Delete",
|
|
870
|
+
"Expr",
|
|
871
|
+
"Global",
|
|
872
|
+
"Import",
|
|
873
|
+
"ImportFrom",
|
|
874
|
+
"Nonlocal",
|
|
875
|
+
"Pass",
|
|
870
876
|
}
|
|
871
877
|
|
|
872
878
|
def node_exits(self, node: ast.AST) -> set[ArcStart]:
|
|
@@ -889,7 +895,7 @@ class AstArcAnalyzer:
|
|
|
889
895
|
node_name = node.__class__.__name__
|
|
890
896
|
handler = cast(
|
|
891
897
|
Optional[Callable[[ast.AST], set[ArcStart]]],
|
|
892
|
-
getattr(self, "_handle__"
|
|
898
|
+
getattr(self, f"_handle__{node_name}", None),
|
|
893
899
|
)
|
|
894
900
|
if handler is not None:
|
|
895
901
|
arc_starts = handler(node)
|
|
@@ -898,7 +904,7 @@ class AstArcAnalyzer:
|
|
|
898
904
|
# statement), or it's something we overlooked.
|
|
899
905
|
if env.TESTING:
|
|
900
906
|
if node_name not in self.OK_TO_DEFAULT:
|
|
901
|
-
raise RuntimeError(f"*** Unhandled: {node}")
|
|
907
|
+
raise RuntimeError(f"*** Unhandled: {node}") # pragma: only failure
|
|
902
908
|
|
|
903
909
|
# Default for simple statements: one exit from this node.
|
|
904
910
|
arc_starts = {ArcStart(self.line_for_node(node))}
|
|
@@ -937,108 +943,12 @@ class AstArcAnalyzer:
|
|
|
937
943
|
for body_node in body:
|
|
938
944
|
lineno = self.line_for_node(body_node)
|
|
939
945
|
if lineno not in self.statements:
|
|
940
|
-
|
|
941
|
-
if maybe_body_node is None:
|
|
942
|
-
continue
|
|
943
|
-
body_node = maybe_body_node
|
|
944
|
-
lineno = self.line_for_node(body_node)
|
|
946
|
+
continue
|
|
945
947
|
for prev_start in prev_starts:
|
|
946
948
|
self.add_arc(prev_start.lineno, lineno, prev_start.cause)
|
|
947
949
|
prev_starts = self.node_exits(body_node)
|
|
948
950
|
return prev_starts
|
|
949
951
|
|
|
950
|
-
def find_non_missing_node(self, node: ast.AST) -> ast.AST | None:
|
|
951
|
-
"""Search `node` looking for a child that has not been optimized away.
|
|
952
|
-
|
|
953
|
-
This might return the node you started with, or it will work recursively
|
|
954
|
-
to find a child node in self.statements.
|
|
955
|
-
|
|
956
|
-
Returns a node, or None if none of the node remains.
|
|
957
|
-
|
|
958
|
-
"""
|
|
959
|
-
# This repeats work just done in process_body, but this duplication
|
|
960
|
-
# means we can avoid a function call in the 99.9999% case of not
|
|
961
|
-
# optimizing away statements.
|
|
962
|
-
lineno = self.line_for_node(node)
|
|
963
|
-
if lineno in self.statements:
|
|
964
|
-
return node
|
|
965
|
-
|
|
966
|
-
missing_fn = cast(
|
|
967
|
-
Optional[Callable[[ast.AST], Optional[ast.AST]]],
|
|
968
|
-
getattr(self, "_missing__" + node.__class__.__name__, None),
|
|
969
|
-
)
|
|
970
|
-
if missing_fn is not None:
|
|
971
|
-
ret_node = missing_fn(node)
|
|
972
|
-
else:
|
|
973
|
-
ret_node = None
|
|
974
|
-
return ret_node
|
|
975
|
-
|
|
976
|
-
# Missing nodes: _missing__*
|
|
977
|
-
#
|
|
978
|
-
# Entire statements can be optimized away by Python. They will appear in
|
|
979
|
-
# the AST, but not the bytecode. These functions are called (by
|
|
980
|
-
# find_non_missing_node) to find a node to use instead of the missing
|
|
981
|
-
# node. They can return None if the node should truly be gone.
|
|
982
|
-
|
|
983
|
-
def _missing__If(self, node: ast.If) -> ast.AST | None:
|
|
984
|
-
# If the if-node is missing, then one of its children might still be
|
|
985
|
-
# here, but not both. So return the first of the two that isn't missing.
|
|
986
|
-
# Use a NodeList to hold the clauses as a single node.
|
|
987
|
-
non_missing = self.find_non_missing_node(NodeList(node.body))
|
|
988
|
-
if non_missing:
|
|
989
|
-
return non_missing
|
|
990
|
-
if node.orelse:
|
|
991
|
-
return self.find_non_missing_node(NodeList(node.orelse))
|
|
992
|
-
return None
|
|
993
|
-
|
|
994
|
-
def _missing__NodeList(self, node: NodeList) -> ast.AST | None:
|
|
995
|
-
# A NodeList might be a mixture of missing and present nodes. Find the
|
|
996
|
-
# ones that are present.
|
|
997
|
-
non_missing_children = []
|
|
998
|
-
for child in node.body:
|
|
999
|
-
maybe_child = self.find_non_missing_node(child)
|
|
1000
|
-
if maybe_child is not None:
|
|
1001
|
-
non_missing_children.append(maybe_child)
|
|
1002
|
-
|
|
1003
|
-
# Return the simplest representation of the present children.
|
|
1004
|
-
if not non_missing_children:
|
|
1005
|
-
return None
|
|
1006
|
-
if len(non_missing_children) == 1:
|
|
1007
|
-
return non_missing_children[0]
|
|
1008
|
-
return NodeList(non_missing_children)
|
|
1009
|
-
|
|
1010
|
-
def _missing__While(self, node: ast.While) -> ast.AST | None:
|
|
1011
|
-
body_nodes = self.find_non_missing_node(NodeList(node.body))
|
|
1012
|
-
if not body_nodes:
|
|
1013
|
-
return None
|
|
1014
|
-
# Make a synthetic While-true node.
|
|
1015
|
-
new_while = ast.While() # type: ignore[call-arg]
|
|
1016
|
-
new_while.lineno = body_nodes.lineno # type: ignore[attr-defined]
|
|
1017
|
-
new_while.test = ast.Name() # type: ignore[call-arg]
|
|
1018
|
-
new_while.test.lineno = body_nodes.lineno # type: ignore[attr-defined]
|
|
1019
|
-
new_while.test.id = "True"
|
|
1020
|
-
assert hasattr(body_nodes, "body")
|
|
1021
|
-
new_while.body = body_nodes.body
|
|
1022
|
-
new_while.orelse = []
|
|
1023
|
-
return new_while
|
|
1024
|
-
|
|
1025
|
-
def is_constant_expr(self, node: ast.AST) -> str | None:
|
|
1026
|
-
"""Is this a compile-time constant?"""
|
|
1027
|
-
node_name = node.__class__.__name__
|
|
1028
|
-
if node_name in ["Constant", "NameConstant", "Num"]:
|
|
1029
|
-
return "Num"
|
|
1030
|
-
elif isinstance(node, ast.Name):
|
|
1031
|
-
if node.id in ["True", "False", "None", "__debug__"]:
|
|
1032
|
-
return "Name"
|
|
1033
|
-
return None
|
|
1034
|
-
|
|
1035
|
-
# In the fullness of time, these might be good tests to write:
|
|
1036
|
-
# while EXPR:
|
|
1037
|
-
# while False:
|
|
1038
|
-
# listcomps hidden deep in other expressions
|
|
1039
|
-
# listcomps hidden in lists: x = [[i for i in range(10)]]
|
|
1040
|
-
# nested function definitions
|
|
1041
|
-
|
|
1042
952
|
# Exit processing: process_*_exits
|
|
1043
953
|
#
|
|
1044
954
|
# These functions process the four kinds of jump exits: break, continue,
|
|
@@ -1049,13 +959,13 @@ class AstArcAnalyzer:
|
|
|
1049
959
|
|
|
1050
960
|
def process_break_exits(self, exits: set[ArcStart]) -> None:
|
|
1051
961
|
"""Add arcs due to jumps from `exits` being breaks."""
|
|
1052
|
-
for block in self.nearest_blocks():
|
|
962
|
+
for block in self.nearest_blocks(): # pragma: always breaks
|
|
1053
963
|
if block.process_break_exits(exits, self.add_arc):
|
|
1054
964
|
break
|
|
1055
965
|
|
|
1056
966
|
def process_continue_exits(self, exits: set[ArcStart]) -> None:
|
|
1057
967
|
"""Add arcs due to jumps from `exits` being continues."""
|
|
1058
|
-
for block in self.nearest_blocks():
|
|
968
|
+
for block in self.nearest_blocks(): # pragma: always breaks
|
|
1059
969
|
if block.process_continue_exits(exits, self.add_arc):
|
|
1060
970
|
break
|
|
1061
971
|
|
|
@@ -1067,7 +977,7 @@ class AstArcAnalyzer:
|
|
|
1067
977
|
|
|
1068
978
|
def process_return_exits(self, exits: set[ArcStart]) -> None:
|
|
1069
979
|
"""Add arcs due to jumps from `exits` being returns."""
|
|
1070
|
-
for block in self.nearest_blocks():
|
|
980
|
+
for block in self.nearest_blocks(): # pragma: always breaks
|
|
1071
981
|
if block.process_return_exits(exits, self.add_arc):
|
|
1072
982
|
break
|
|
1073
983
|
|
|
@@ -1097,8 +1007,8 @@ class AstArcAnalyzer:
|
|
|
1097
1007
|
last = None
|
|
1098
1008
|
for dec_node in decs:
|
|
1099
1009
|
dec_start = self.line_for_node(dec_node)
|
|
1100
|
-
if last is not None and dec_start != last:
|
|
1101
|
-
self.add_arc(last, dec_start)
|
|
1010
|
+
if last is not None and dec_start != last:
|
|
1011
|
+
self.add_arc(last, dec_start)
|
|
1102
1012
|
last = dec_start
|
|
1103
1013
|
assert last is not None
|
|
1104
1014
|
self.add_arc(last, main_line)
|
|
@@ -1147,48 +1057,44 @@ class AstArcAnalyzer:
|
|
|
1147
1057
|
|
|
1148
1058
|
def _handle__If(self, node: ast.If) -> set[ArcStart]:
|
|
1149
1059
|
start = self.line_for_node(node.test)
|
|
1150
|
-
|
|
1151
|
-
exits =
|
|
1152
|
-
|
|
1153
|
-
|
|
1060
|
+
constant_test, val = is_constant_test_expr(node.test)
|
|
1061
|
+
exits = set()
|
|
1062
|
+
if not constant_test or val:
|
|
1063
|
+
from_start = ArcStart(start, cause="the condition on line {lineno} was never true")
|
|
1064
|
+
exits |= self.process_body(node.body, from_start=from_start)
|
|
1065
|
+
if not constant_test or not val:
|
|
1066
|
+
from_start = ArcStart(start, cause="the condition on line {lineno} was always true")
|
|
1067
|
+
exits |= self.process_body(node.orelse, from_start=from_start)
|
|
1154
1068
|
return exits
|
|
1155
1069
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
cause="the pattern on line {lineno} never matched",
|
|
1167
|
-
)
|
|
1168
|
-
exits |= self.process_body(case.body, from_start=from_start)
|
|
1169
|
-
last_start = case_start
|
|
1170
|
-
|
|
1171
|
-
# case is now the last case, check for wildcard match.
|
|
1172
|
-
pattern = case.pattern # pylint: disable=undefined-loop-variable
|
|
1173
|
-
while isinstance(pattern, ast.MatchOr):
|
|
1174
|
-
pattern = pattern.patterns[-1]
|
|
1175
|
-
while isinstance(pattern, ast.MatchAs) and pattern.pattern is not None:
|
|
1176
|
-
pattern = pattern.pattern
|
|
1177
|
-
had_wildcard = (
|
|
1178
|
-
isinstance(pattern, ast.MatchAs)
|
|
1179
|
-
and pattern.pattern is None
|
|
1180
|
-
and case.guard is None # pylint: disable=undefined-loop-variable
|
|
1070
|
+
def _handle__Match(self, node: ast.Match) -> set[ArcStart]:
|
|
1071
|
+
start = self.line_for_node(node)
|
|
1072
|
+
last_start = start
|
|
1073
|
+
exits = set()
|
|
1074
|
+
for case in node.cases:
|
|
1075
|
+
case_start = self.line_for_node(case.pattern)
|
|
1076
|
+
self.add_arc(last_start, case_start, "the pattern on line {lineno} always matched")
|
|
1077
|
+
from_start = ArcStart(
|
|
1078
|
+
case_start,
|
|
1079
|
+
cause="the pattern on line {lineno} never matched",
|
|
1181
1080
|
)
|
|
1081
|
+
exits |= self.process_body(case.body, from_start=from_start)
|
|
1082
|
+
last_start = case_start
|
|
1083
|
+
|
|
1084
|
+
# case is now the last case, check for wildcard match.
|
|
1085
|
+
pattern = case.pattern # pylint: disable=undefined-loop-variable
|
|
1086
|
+
while isinstance(pattern, ast.MatchOr):
|
|
1087
|
+
pattern = pattern.patterns[-1]
|
|
1088
|
+
while isinstance(pattern, ast.MatchAs) and pattern.pattern is not None:
|
|
1089
|
+
pattern = pattern.pattern
|
|
1090
|
+
had_wildcard = (
|
|
1091
|
+
isinstance(pattern, ast.MatchAs) and pattern.pattern is None and case.guard is None # pylint: disable=undefined-loop-variable
|
|
1092
|
+
)
|
|
1182
1093
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
return exits
|
|
1188
|
-
|
|
1189
|
-
def _handle__NodeList(self, node: NodeList) -> set[ArcStart]:
|
|
1190
|
-
start = self.line_for_node(node)
|
|
1191
|
-
exits = self.process_body(node.body, from_start=ArcStart(start))
|
|
1094
|
+
if not had_wildcard:
|
|
1095
|
+
exits.add(
|
|
1096
|
+
ArcStart(case_start, cause="the pattern on line {lineno} always matched"),
|
|
1097
|
+
)
|
|
1192
1098
|
return exits
|
|
1193
1099
|
|
|
1194
1100
|
def _handle__Raise(self, node: ast.Raise) -> set[ArcStart]:
|
|
@@ -1260,16 +1166,11 @@ class AstArcAnalyzer:
|
|
|
1260
1166
|
|
|
1261
1167
|
return exits
|
|
1262
1168
|
|
|
1169
|
+
_handle__TryStar = _handle__Try
|
|
1170
|
+
|
|
1263
1171
|
def _handle__While(self, node: ast.While) -> set[ArcStart]:
|
|
1264
1172
|
start = to_top = self.line_for_node(node.test)
|
|
1265
|
-
constant_test =
|
|
1266
|
-
top_is_body0 = False
|
|
1267
|
-
if constant_test:
|
|
1268
|
-
top_is_body0 = True
|
|
1269
|
-
if env.PYBEHAVIOR.keep_constant_test:
|
|
1270
|
-
top_is_body0 = False
|
|
1271
|
-
if top_is_body0:
|
|
1272
|
-
to_top = self.line_for_node(node.body[0])
|
|
1173
|
+
constant_test, _ = is_constant_test_expr(node.test)
|
|
1273
1174
|
self.block_stack.append(LoopBlock(start=to_top))
|
|
1274
1175
|
from_start = ArcStart(start, cause="the condition on line {lineno} was never true")
|
|
1275
1176
|
exits = self.process_body(node.body, from_start=from_start)
|
|
@@ -1294,22 +1195,20 @@ class AstArcAnalyzer:
|
|
|
1294
1195
|
starts = [self.line_for_node(item.context_expr) for item in node.items]
|
|
1295
1196
|
else:
|
|
1296
1197
|
starts = [self.line_for_node(node)]
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
self.all_with_starts.add(start)
|
|
1198
|
+
for start in starts:
|
|
1199
|
+
self.current_with_starts.add(start)
|
|
1200
|
+
self.all_with_starts.add(start)
|
|
1301
1201
|
|
|
1302
1202
|
exits = self.process_body(node.body, from_start=ArcStart(starts[-1]))
|
|
1303
1203
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
exits = with_exit
|
|
1204
|
+
start = starts[-1]
|
|
1205
|
+
self.current_with_starts.remove(start)
|
|
1206
|
+
with_exit = {ArcStart(start)}
|
|
1207
|
+
if exits:
|
|
1208
|
+
for xit in exits:
|
|
1209
|
+
self.add_arc(xit.lineno, start)
|
|
1210
|
+
self.with_exits.add((xit.lineno, start))
|
|
1211
|
+
exits = with_exit
|
|
1313
1212
|
|
|
1314
1213
|
return exits
|
|
1315
1214
|
|