Skip to content

Commit f7b71e7

Browse files
committed
fix: don't confuse a named match with a wildcard match
1 parent 17964c4 commit f7b71e7

File tree

4 files changed

+76
-8
lines changed

4 files changed

+76
-8
lines changed

CHANGES.rst

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ Unreleased
3030
the problem. They are now changed to mention "branch coverage data" and
3131
"statement coverage data."
3232

33+
- Fixed a minor branch coverage problem with wildcard match/case cases using
34+
names or guard clauses.
35+
3336
- Started testing on 3.13 free-threading (nogil) builds of Python. I'm not
3437
claiming full support yet.
3538

coverage/parser.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -1165,21 +1165,26 @@ def _handle__Match(self, node: ast.Match) -> set[ArcStart]:
11651165
start = self.line_for_node(node)
11661166
last_start = start
11671167
exits = set()
1168-
had_wildcard = False
11691168
for case in node.cases:
11701169
case_start = self.line_for_node(case.pattern)
1171-
pattern = case.pattern
1172-
while isinstance(pattern, ast.MatchOr):
1173-
pattern = pattern.patterns[-1]
1174-
if isinstance(pattern, ast.MatchAs):
1175-
had_wildcard = True
11761170
self.add_arc(last_start, case_start, "the pattern on line {lineno} always matched")
11771171
from_start = ArcStart(
11781172
case_start,
11791173
cause="the pattern on line {lineno} never matched",
11801174
)
11811175
exits |= self.body_exits(case.body, from_start=from_start)
11821176
last_start = case_start
1177+
1178+
# case is now the last case, check for wildcard match.
1179+
pattern = case.pattern # pylint: disable=undefined-loop-variable
1180+
while isinstance(pattern, ast.MatchOr):
1181+
pattern = pattern.patterns[-1]
1182+
had_wildcard = (
1183+
isinstance(pattern, ast.MatchAs)
1184+
and pattern.pattern is None
1185+
and case.guard is None # pylint: disable=undefined-loop-variable
1186+
)
1187+
11831188
if not had_wildcard:
11841189
exits.add(
11851190
ArcStart(case_start, cause="the pattern on line {lineno} always matched"),

tests/test_arcs.py

+39-1
Original file line numberDiff line numberDiff line change
@@ -1375,7 +1375,7 @@ def test_match_case_without_wildcard(self) -> None:
13751375
)
13761376
assert self.stdout() == "None\nno go\ngo: n\n"
13771377

1378-
def test_absurd_wildcard(self) -> None:
1378+
def test_absurd_wildcards(self) -> None:
13791379
# https://github.com/nedbat/coveragepy/issues/1421
13801380
self.check_coverage("""\
13811381
def absurd(x):
@@ -1384,9 +1384,47 @@ def absurd(x):
13841384
print("default")
13851385
absurd(5)
13861386
""",
1387+
# No arc from 3 to 5 because 3 always matches.
13871388
arcz=".1 15 5. .2 23 34 4.",
13881389
)
13891390
assert self.stdout() == "default\n"
1391+
self.check_coverage("""\
1392+
def absurd(x):
1393+
match x:
1394+
case (3 | 99 | 999 as y):
1395+
print("not default")
1396+
absurd(5)
1397+
""",
1398+
arcz=".1 15 5. .2 23 34 3. 4.",
1399+
arcz_missing="34 4.",
1400+
)
1401+
assert self.stdout() == ""
1402+
self.check_coverage("""\
1403+
def absurd(x):
1404+
match x:
1405+
case (3 | 17 as y):
1406+
print("not default")
1407+
case 7: # 5
1408+
print("also not default")
1409+
absurd(7)
1410+
""",
1411+
arcz=".1 17 7. .2 23 34 4. 35 56 5. 6.",
1412+
arcz_missing="34 4. 5.",
1413+
)
1414+
assert self.stdout() == "also not default\n"
1415+
self.check_coverage("""\
1416+
def absurd(x):
1417+
match x:
1418+
case 3:
1419+
print("not default")
1420+
case _ if x == 7: # 5
1421+
print("also not default")
1422+
absurd(7)
1423+
""",
1424+
arcz=".1 17 7. .2 23 34 4. 35 56 5. 6.",
1425+
arcz_missing="34 4. 5.",
1426+
)
1427+
assert self.stdout() == "also not default\n"
13901428

13911429

13921430
class OptimizedIfTest(CoverageTest):

tests/test_parser.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,11 @@ def test_missing_arc_descriptions_bug460(self) -> None:
946946
""")
947947
assert parser.missing_arc_description(2, -3) == "line 3 didn't finish the lambda on line 3"
948948

949-
@pytest.mark.skipif(not env.PYBEHAVIOR.match_case, reason="Match-case is new in 3.10")
949+
950+
@pytest.mark.skipif(not env.PYBEHAVIOR.match_case, reason="Match-case is new in 3.10")
951+
class MatchCaseMissingArcDescriptionTest(PythonParserTestBase):
952+
"""Missing arc descriptions for match/case."""
953+
950954
def test_match_case(self) -> None:
951955
parser = self.parse_text("""\
952956
match command.split():
@@ -966,6 +970,24 @@ def test_match_case(self) -> None:
966970
"line 4 didn't jump to line 6 because the pattern on line 4 always matched"
967971
)
968972

973+
def test_final_wildcard(self) -> None:
974+
parser = self.parse_text("""\
975+
match command.split():
976+
case ["go", direction] if direction in "nesw": # 2
977+
match = f"go: {direction}"
978+
case _: # 4
979+
match = "no go"
980+
print(match) # 6
981+
""")
982+
assert parser.missing_arc_description(2, 3) == (
983+
"line 2 didn't jump to line 3 because the pattern on line 2 never matched"
984+
)
985+
assert parser.missing_arc_description(2, 4) == (
986+
"line 2 didn't jump to line 4 because the pattern on line 2 always matched"
987+
)
988+
# 4-6 isn't a possible arc, so the description is generic.
989+
assert parser.missing_arc_description(4, 6) == "line 4 didn't jump to line 6"
990+
969991

970992
class ParserFileTest(CoverageTest):
971993
"""Tests for coverage.py's code parsing from files."""

0 commit comments

Comments
 (0)