Skip to content

Commit 794435e

Browse files
Fix rule hierarchy bug: FLY is not a subrule of F, require digit boundary
Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/94e8b825-bb91-4ef3-9763-5f4a59375e86
1 parent 397f896 commit 794435e

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

src/usethis/_tool/rule.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,24 @@ def is_rule_covered_by(rule: Rule, parent: Rule) -> bool:
2121
For example, "TC001" is covered by "TC", and any rule is covered by "ALL".
2222
2323
A rule does not cover itself. "ALL" is never covered by a specific rule.
24+
25+
Rule codes consist of a letter prefix (the group) followed by optional digits
26+
(the specific rule). A parent only covers a child if they share the same letter
27+
prefix; for example, "F" covers "F101" but not "FLY" or "FLY001".
2428
"""
2529
if parent == rule:
2630
return False
2731
if parent == "ALL":
2832
return True
2933
if rule == "ALL":
3034
return False
31-
return rule.startswith(parent)
35+
if not rule.startswith(parent):
36+
return False
37+
# After the parent prefix, the next character in the child (if any) must be a
38+
# digit. This prevents "F" from covering "FLY" (next char 'L' is a letter,
39+
# meaning FLY is a separate rule group).
40+
rest = rule[len(parent) :]
41+
return rest == "" or rest[0].isdigit()
3242

3343

3444
@dataclass(frozen=True)

tests/usethis/_tool/test_rule.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@ def test_partial_prefix(self):
2323
def test_unrelated(self):
2424
assert not is_rule_covered_by("E501", "TC")
2525

26+
def test_different_group_same_first_letter(self):
27+
# FLY is a separate rule group from F
28+
assert not is_rule_covered_by("FLY", "F")
29+
30+
def test_different_group_specific_rule(self):
31+
# FLY001 belongs to FLY, not F
32+
assert not is_rule_covered_by("FLY001", "F")
33+
34+
def test_same_group_digit_prefix(self):
35+
# F101 is a subrule of F1
36+
assert is_rule_covered_by("F101", "F1")
37+
38+
def test_same_group_specific_from_group(self):
39+
# F101 is a subrule of F
40+
assert is_rule_covered_by("F101", "F")
41+
2642
def test_all_does_not_cover_itself(self):
2743
assert not is_rule_covered_by("ALL", "ALL")
2844

@@ -93,6 +109,24 @@ def test_preserves_unrelated_existing(self):
93109
assert result.to_add == ["C"]
94110
assert result.to_remove == []
95111

112+
def test_different_group_same_first_letter_not_absorbed(self):
113+
# "FLY" should not be absorbed by existing "F"
114+
result = reconcile_rules(["F"], ["FLY"])
115+
assert result.to_add == ["FLY"]
116+
assert result.to_remove == []
117+
118+
def test_group_does_not_replace_different_group(self):
119+
# "F" should not replace existing "FLY001"
120+
result = reconcile_rules(["FLY001"], ["F"])
121+
assert result.to_add == ["F"]
122+
assert result.to_remove == []
123+
124+
def test_incoming_dedup_different_groups(self):
125+
# "F" and "FLY" incoming; both should survive since they're different groups
126+
result = reconcile_rules([], ["F", "FLY"])
127+
assert result.to_add == ["F", "FLY"]
128+
assert result.to_remove == []
129+
96130
def test_is_noop_when_no_changes(self):
97131
result = reconcile_rules(["A"], ["A"])
98132
assert result.is_noop

0 commit comments

Comments
 (0)