diff --git a/tests/v2/test_differential.py b/tests/v2/test_differential.py index 0daeacb..b576539 100644 --- a/tests/v2/test_differential.py +++ b/tests/v2/test_differential.py @@ -961,8 +961,8 @@ def test_dormant_rules_reports_a_rule_whose_behavior_vanished() -> None: report = compare.dormant_rules( rules, {"fix(a)"}, [("John Smith", {"given"})]) assert report.awake == () - assert [i for i, _ in report.undeclared] == ["fix(b)"] - assert "reverted" in report.undeclared[0][1] + assert [d.issue for d in report.undeclared] == ["fix(b)"] + assert report.undeclared[0].kind == "reverted" def test_dormant_rules_names_the_rule_that_shadows_one() -> None: @@ -976,8 +976,9 @@ def test_dormant_rules_names_the_rule_that_shadows_one() -> None: "fields": ["given"]}] report = compare.dormant_rules( rules, {"fix(broad)"}, [("John Smith", {"given"})]) - assert [i for i, _ in report.undeclared] == ["fix(narrow)"] - assert "shadowed by 'fix(broad)'" in report.undeclared[0][1] + assert [d.issue for d in report.undeclared] == ["fix(narrow)"] + assert report.undeclared[0].kind == "shadowed" + assert report.undeclared[0].detail == "fix(broad)" def test_dormant_rules_distinguishes_an_excluded_shape() -> None: @@ -988,8 +989,8 @@ def test_dormant_rules_distinguishes_an_excluded_shape() -> None: never = [{"why": "protected", "name_regex": "Smith"}] report = compare.dormant_rules( rules, set(), [("John Smith", {"given"})], never) - assert [i for i, _ in report.undeclared] == ["fix(a)"] - assert "[[never]]" in report.undeclared[0][1] + assert [d.issue for d in report.undeclared] == ["fix(a)"] + assert report.undeclared[0].kind == "excluded" def test_dormant_rules_is_silent_about_a_declared_rule() -> None: @@ -1022,8 +1023,8 @@ def test_dormant_rules_names_the_shadower_that_does_the_shadowing() -> None: rules, {"fix(a)", "fix(z)"}, [("Alpha One", {"given"}), ("Zeta One", {"given"}), ("Zeta Two", {"given"}), ("Zeta Three", {"given"})]) - assert [i for i, _ in report.undeclared] == ["fix(idle)"] - assert "shadowed by 'fix(z)'" in report.undeclared[0][1] + assert [d.issue for d in report.undeclared] == ["fix(idle)"] + assert report.undeclared[0].detail == "fix(z)" def test_dormant_rules_sorts_before_diagnosing() -> None: @@ -1041,8 +1042,8 @@ def test_dormant_rules_sorts_before_diagnosing() -> None: "fields": ["given"]}] report = compare.dormant_rules( rules, {"specific"}, [("John Smith", {"given"})]) - assert [i for i, _ in report.undeclared] == ["broad"] - assert "shadowed by 'specific'" in report.undeclared[0][1] + assert [d.issue for d in report.undeclared] == ["broad"] + assert report.undeclared[0].detail == "specific" def test_validate_rules_rejects_two_rules_sharing_an_issue() -> None: diff --git a/tools/differential/compare.py b/tools/differential/compare.py index fe3c043..afda334 100644 --- a/tools/differential/compare.py +++ b/tools/differential/compare.py @@ -19,7 +19,7 @@ import tomllib from collections import Counter from pathlib import Path -from typing import NamedTuple +from typing import Literal, NamedTuple HERE = Path(__file__).resolve().parent FIELDS = ("title", "first", "middle", "last", "suffix", "nickname", @@ -639,11 +639,37 @@ def classify(name: str, diff_fields: set[str], return None +#: How each `_Dormant.kind` reads in the report. Here rather than in +#: dormant_rules so the computation never has to know the wording, and +#: rephrasing one is a change to output alone. +_DORMANT_WHY = { + "reverted": ("matched no diffing name -- the behavior it describes " + "may have been reverted"), + "shadowed": "shadowed by {by}", + "excluded": ("every diffing name it matches is refused by a " + "[[never]] exclusion"), +} + + +class _Dormant(NamedTuple): + """One rule that explained nothing, and why. + + `kind` rather than a sentence because the three have three different + fixes, and a caller that wants to tell them apart should not have to + parse prose to do it. The wording lives in main(), which is the only + place that renders it -- so rephrasing a diagnosis stays a change to + output alone, and the tests that pin the DISTINCTION keep working. + """ + issue: str + kind: Literal["reverted", "shadowed", "excluded"] + #: the issue that claimed it, when `kind` is "shadowed"; else "" + detail: str + + class _Dormancy(NamedTuple): """What a run found out about rules that explained nothing.""" - #: (issue, diagnosis) for every rule that explained nothing and does - #: not declare `dormant` - undeclared: tuple[tuple[str, str], ...] + #: every rule that explained nothing and does not declare `dormant` + undeclared: tuple[_Dormant, ...] #: issues declaring `dormant` that explained at least one diff awake: tuple[str, ...] @@ -690,18 +716,16 @@ def dormant_rules(rules: list[dict[str, object]], explained: set[str], continue matched = [(n, d) for n, d in diffing if _entry_matches(rule, n, d)] + winners = Counter( + c for c in (classify(n, d, ordered, exclusions) + for n, d in matched) if c is not None) if not matched: - why = ("matched no diffing name -- the behavior it describes " - "may have been reverted") + undeclared.append(_Dormant(issue, "reverted", "")) + elif winners: + undeclared.append( + _Dormant(issue, "shadowed", winners.most_common(1)[0][0])) else: - winners = Counter( - c for c in (classify(n, d, ordered, exclusions) - for n, d in matched) if c is not None) - why = (f"shadowed by {winners.most_common(1)[0][0]!r}" - if winners else - "every diffing name it matches is refused by a " - "[[never]] exclusion") - undeclared.append((issue, why)) + undeclared.append(_Dormant(issue, "excluded", "")) return _Dormancy(tuple(undeclared), tuple(awake)) @@ -833,8 +857,9 @@ def main() -> int: print(f" {n!r}") print() dormancy = dormant_rules(rules, set(by_issue), diffing, exclusions) - for issue, why in dormancy.undeclared: - print(f"EXPLAINED NOTHING {issue!r}\n {why}") + for dormant in dormancy.undeclared: + print(f"EXPLAINED NOTHING {dormant.issue!r}\n " + f"{_DORMANT_WHY[dormant.kind].format(by=repr(dormant.detail))}") for issue in dormancy.awake: print(f"NO LONGER DORMANT {issue!r}\n it explained a diff in " f"this run, so its `dormant` reason is now false -- remove "