Skip to content

Commit

Permalink
fix: don't confuse a named match with a wildcard match
Browse files Browse the repository at this point in the history
  • Loading branch information
nedbat committed Jun 17, 2024
1 parent 17964c4 commit f7b71e7
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 8 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ Unreleased
the problem. They are now changed to mention "branch coverage data" and
"statement coverage data."

- Fixed a minor branch coverage problem with wildcard match/case cases using
names or guard clauses.

- Started testing on 3.13 free-threading (nogil) builds of Python. I'm not
claiming full support yet.

Expand Down
17 changes: 11 additions & 6 deletions coverage/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1165,21 +1165,26 @@ def _handle__Match(self, node: ast.Match) -> set[ArcStart]:
start = self.line_for_node(node)
last_start = start
exits = set()
had_wildcard = False
for case in node.cases:
case_start = self.line_for_node(case.pattern)
pattern = case.pattern
while isinstance(pattern, ast.MatchOr):
pattern = pattern.patterns[-1]
if isinstance(pattern, ast.MatchAs):
had_wildcard = True
self.add_arc(last_start, case_start, "the pattern on line {lineno} always matched")
from_start = ArcStart(
case_start,
cause="the pattern on line {lineno} never matched",
)
exits |= self.body_exits(case.body, from_start=from_start)
last_start = case_start

# case is now the last case, check for wildcard match.
pattern = case.pattern # pylint: disable=undefined-loop-variable
while isinstance(pattern, ast.MatchOr):
pattern = pattern.patterns[-1]
had_wildcard = (
isinstance(pattern, ast.MatchAs)
and pattern.pattern is None
and case.guard is None # pylint: disable=undefined-loop-variable
)

if not had_wildcard:
exits.add(
ArcStart(case_start, cause="the pattern on line {lineno} always matched"),
Expand Down
40 changes: 39 additions & 1 deletion tests/test_arcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1375,7 +1375,7 @@ def test_match_case_without_wildcard(self) -> None:
)
assert self.stdout() == "None\nno go\ngo: n\n"

def test_absurd_wildcard(self) -> None:
def test_absurd_wildcards(self) -> None:
# https://github.com/nedbat/coveragepy/issues/1421
self.check_coverage("""\
def absurd(x):
Expand All @@ -1384,9 +1384,47 @@ def absurd(x):
print("default")
absurd(5)
""",
# No arc from 3 to 5 because 3 always matches.
arcz=".1 15 5. .2 23 34 4.",
)
assert self.stdout() == "default\n"
self.check_coverage("""\
def absurd(x):
match x:
case (3 | 99 | 999 as y):
print("not default")
absurd(5)
""",
arcz=".1 15 5. .2 23 34 3. 4.",
arcz_missing="34 4.",
)
assert self.stdout() == ""
self.check_coverage("""\
def absurd(x):
match x:
case (3 | 17 as y):
print("not default")
case 7: # 5
print("also not default")
absurd(7)
""",
arcz=".1 17 7. .2 23 34 4. 35 56 5. 6.",
arcz_missing="34 4. 5.",
)
assert self.stdout() == "also not default\n"
self.check_coverage("""\
def absurd(x):
match x:
case 3:
print("not default")
case _ if x == 7: # 5
print("also not default")
absurd(7)
""",
arcz=".1 17 7. .2 23 34 4. 35 56 5. 6.",
arcz_missing="34 4. 5.",
)
assert self.stdout() == "also not default\n"


class OptimizedIfTest(CoverageTest):
Expand Down
24 changes: 23 additions & 1 deletion tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -946,7 +946,11 @@ def test_missing_arc_descriptions_bug460(self) -> None:
""")
assert parser.missing_arc_description(2, -3) == "line 3 didn't finish the lambda on line 3"

@pytest.mark.skipif(not env.PYBEHAVIOR.match_case, reason="Match-case is new in 3.10")

@pytest.mark.skipif(not env.PYBEHAVIOR.match_case, reason="Match-case is new in 3.10")
class MatchCaseMissingArcDescriptionTest(PythonParserTestBase):
"""Missing arc descriptions for match/case."""

def test_match_case(self) -> None:
parser = self.parse_text("""\
match command.split():
Expand All @@ -966,6 +970,24 @@ def test_match_case(self) -> None:
"line 4 didn't jump to line 6 because the pattern on line 4 always matched"
)

def test_final_wildcard(self) -> None:
parser = self.parse_text("""\
match command.split():
case ["go", direction] if direction in "nesw": # 2
match = f"go: {direction}"
case _: # 4
match = "no go"
print(match) # 6
""")
assert parser.missing_arc_description(2, 3) == (
"line 2 didn't jump to line 3 because the pattern on line 2 never matched"
)
assert parser.missing_arc_description(2, 4) == (
"line 2 didn't jump to line 4 because the pattern on line 2 always matched"
)
# 4-6 isn't a possible arc, so the description is generic.
assert parser.missing_arc_description(4, 6) == "line 4 didn't jump to line 6"


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

0 comments on commit f7b71e7

Please sign in to comment.