Skip to content

Commit ce30693

Browse files
authored
Merge pull request #155 from Tobias-Fischer/fix/sort-vinca-lists-duplicate-conditions
fix: reject duplicate if-block conditions within the same list key
2 parents b17a437 + 3c5e1b2 commit ce30693

2 files changed

Lines changed: 114 additions & 9 deletions

File tree

vinca/sort_vinca_lists.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
Conditional blocks (`- if: ... then: [...]`) stay at the end; their inner
66
`then:` lists are also sorted.
77
8+
Also validates that each condition (e.g. "win", "not win", "linux") appears
9+
in at most one `- if:` block per top-level list key. Two separate blocks for
10+
the same condition are never wrong on their own, but they're a standing
11+
foot-gun: a future addition can land in the "wrong" one by accident, and
12+
nothing merges the two, so the same package can end up skipped from one
13+
angle and not the other depending on which block someone edits. Raises
14+
DuplicateConditionError (exit 1) rather than silently merging them, since
15+
merging by hand needs a human to reconcile any per-item comments correctly.
16+
817
Usage:
918
vinca-sort-vinca-lists [FILE]
1019
vinca-sort-vinca-lists --check [FILE]
@@ -27,12 +36,19 @@
2736
RE_SIMPLE_ITEM = re.compile(r"^ - (\S.*)$")
2837
# Regex for the start of a conditional block: " - if: ..."
2938
RE_IF_BLOCK = re.compile(r"^ - if:")
39+
# Regex capturing the condition text of a " - if: <condition>" line
40+
RE_IF_CONDITION = re.compile(r"^ - if:\s*(.+?)\s*$")
3041
# Regex for a then-list item inside a conditional block: " - value"
3142
RE_THEN_ITEM = re.compile(r"^ - (\S.*)$")
3243
# Regex for a top-level key
3344
RE_TOP_KEY = re.compile(r"^(\S+):")
3445

3546

47+
class DuplicateConditionError(ValueError):
48+
"""Raised when the same `- if:` condition appears in more than one block
49+
under the same top-level list key."""
50+
51+
3652
def _sort_key(line: str) -> str:
3753
"""Extract sortable value from a list item line (lowercase, ignore comments)."""
3854
m = RE_SIMPLE_ITEM.match(line) or RE_THEN_ITEM.match(line)
@@ -57,6 +73,7 @@ def sort_vinca_lists(path: Path) -> bool:
5773
# Check if this line starts a target list key
5874
m = RE_TOP_KEY.match(line)
5975
if m and m.group(1) in LISTS_TO_SORT:
76+
list_key = m.group(1)
6077
result.append(line)
6178
i += 1
6279

@@ -136,6 +153,25 @@ def sort_vinca_lists(path: Path) -> bool:
136153
if current_if_block is not None:
137154
if_blocks.append(current_if_block)
138155

156+
# Reject duplicate conditions: two separate "- if:" blocks for the
157+
# same condition under the same list key. See module docstring.
158+
seen_conditions = {}
159+
for block in if_blocks:
160+
cond_match = RE_IF_CONDITION.match(block[0])
161+
if not cond_match:
162+
continue
163+
condition = cond_match.group(1)
164+
if condition in seen_conditions:
165+
raise DuplicateConditionError(
166+
f"{list_key}: condition {condition!r} appears in more "
167+
f"than one '- if:' block. Merge them into a single "
168+
f"block (each item may need its own comment moved "
169+
f"inline first, since sorting the merged then: list "
170+
f"can otherwise separate a standalone comment from "
171+
f"the item it was meant to describe)."
172+
)
173+
seen_conditions[condition] = True
174+
139175
# Sort simple items
140176
sorted_simple = sorted(simple_items, key=_sort_key)
141177
if sorted_simple != simple_items:
@@ -221,14 +257,20 @@ def main():
221257
print(f"ERROR: {args.file} not found", file=sys.stderr)
222258
sys.exit(1)
223259

224-
if args.check:
225-
with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as tf:
226-
tmp = Path(tf.name)
227-
shutil.copy2(args.file, tmp)
228-
changed = sort_vinca_lists(tmp)
229-
tmp.unlink(missing_ok=True)
230-
else:
231-
changed = sort_vinca_lists(args.file)
260+
try:
261+
if args.check:
262+
with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as tf:
263+
tmp = Path(tf.name)
264+
shutil.copy2(args.file, tmp)
265+
try:
266+
changed = sort_vinca_lists(tmp)
267+
finally:
268+
tmp.unlink(missing_ok=True)
269+
else:
270+
changed = sort_vinca_lists(args.file)
271+
except DuplicateConditionError as e:
272+
print(f"ERROR: {args.file}: {e}", file=sys.stderr)
273+
sys.exit(1)
232274

233275
status = ("UNSORTED" if args.check else "SORTED") if changed else "OK"
234276
print(f"{status}: {args.file}")

vinca/test_sort_vinca_lists.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Tests for the vinca.yaml list sorter."""
22

3-
from vinca.sort_vinca_lists import sort_vinca_lists
3+
import pytest
4+
5+
from vinca.sort_vinca_lists import DuplicateConditionError, sort_vinca_lists
46

57
BASE = """packages_select_by_deps:
68
- alpha
@@ -96,3 +98,64 @@ def test_adjacent_if_blocks_separated_by_comment_stay_isolated(tmp_path):
9698
first_block, second_block = path.read_text().split("- if: linux and not aarch64")
9799
assert "- webots_ros2" not in first_block
98100
assert "- webots_ros2" in second_block
101+
102+
103+
def test_duplicate_condition_in_same_list_raises(tmp_path):
104+
# Two non-adjacent "- if: win" blocks under the same list key, separated
105+
# by unrelated content -- not the adjacency bug from the previous fix,
106+
# just two blocks that should have been one all along.
107+
content = """packages_select_by_deps:
108+
- if: win
109+
then:
110+
- alpha
111+
112+
- unrelated_package
113+
114+
- if: win
115+
then:
116+
- bravo
117+
"""
118+
path = tmp_path / "vinca.yaml"
119+
path.write_text(content)
120+
121+
with pytest.raises(
122+
DuplicateConditionError, match=r"packages_select_by_deps.*'win'"
123+
):
124+
sort_vinca_lists(path)
125+
126+
127+
def test_duplicate_condition_scoped_per_list_key(tmp_path):
128+
# The same condition appearing once in each of two different list keys
129+
# is fine -- duplication is only a problem within a single list key.
130+
content = """packages_skip_by_deps:
131+
- if: win
132+
then:
133+
- alpha
134+
135+
packages_select_by_deps:
136+
- if: win
137+
then:
138+
- bravo
139+
"""
140+
path = tmp_path / "vinca.yaml"
141+
path.write_text(content)
142+
143+
# Should not raise.
144+
sort_vinca_lists(path)
145+
146+
147+
def test_different_conditions_do_not_raise(tmp_path):
148+
content = """packages_select_by_deps:
149+
- if: win
150+
then:
151+
- alpha
152+
153+
- if: not win
154+
then:
155+
- bravo
156+
"""
157+
path = tmp_path / "vinca.yaml"
158+
path.write_text(content)
159+
160+
# Should not raise.
161+
sort_vinca_lists(path)

0 commit comments

Comments
 (0)