Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
check_list_order.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Catch the doubled g_list_prepend across a repository boundary.
3
4Splitting a cursor loop into "repository builds the list / domain post-processes it" is the
5standard move of the src/database migration, and it has a standing trap: BOTH halves prepend.
6Two prepends cancel, so the public function silently returns its list in the opposite order to
7the single-loop version it replaced -- with byte-identical SQL, a clean build in every
8configuration, and green CI. It has bitten twice:
9
10 * dt_tag_get_images() / dt_tag_get_images_from_list(), where the repository dropped the
11 reversal the callers used to do (fixed in "database: restore the list order
12 dt_tag_get_images() returns");
13 * dt_map_location_get_locations_by_path() and _map_location_find_images(), where the
14 repository kept reverse-row order AND the caller prepended again.
15
16The rule this enforces: a repository function that builds its result with g_list_prepend and
17returns it WITHOUT reversing hands back reverse-row order, which is only correct when its
18consumer passes the list straight through. If the consumer prepends again, exactly one of the
19two must reverse.
20
21Reported as a list that must stay empty -- not a ratchet. There is no legitimate instance:
22where the flip is genuinely wanted, reverse in the repository and say so, so the intent is in
23the code rather than in the interaction of two files.
24"""
25
26import re
27import subprocess
28import sys
29from pathlib import Path
30
31
32def _functions(text):
33 """(name, body) for every function definition, brace-matched, strings and comments skipped."""
34 out = []
35 for m in re.finditer(r"\n((?:[\w][\w \*]*?)\b(\w+)\s*\‍([^;{]*?\‍)\s*\n?\{)", text):
36 try:
37 i = text.index("{", m.start(1))
38 except ValueError:
39 continue
40 depth, n, closed = 0, len(text), False
41 while i < n:
42 c = text[i]
43 if c in "\"'":
44 quote = c
45 i += 1
46 while i < n and text[i] != quote:
47 i += 2 if text[i] == "\\" else 1
48 elif text.startswith("//", i):
49 i = text.index("\n", i)
50 elif text.startswith("/*", i):
51 i = text.index("*/", i) + 2
52 elif c == "{":
53 depth += 1
54 elif c == "}":
55 depth -= 1
56 if depth == 0:
57 closed = True
58 break
59 i += 1
60 if closed:
61 out.append((m.group(2), text[m.end():i]))
62 return out
63
64
65def main(argv):
66 src = Path(argv[1] if len(argv) > 1 else "src")
67
68 # 1. repository functions that prepend and never reverse -> they return reverse-row order
69 unreversed = {}
70 for path in sorted((src / "database").glob("*_repository.c")):
71 for name, body in _functions(path.read_text(encoding="utf-8", errors="replace")):
72 if "g_list_prepend" in body and "g_list_reverse" not in body:
73 unreversed[name] = path
74
75 if not unreversed:
76 print("OK: no unreversed prepend-built lists leave src/database.")
77 return 0
78
79 # 2. consumers outside the module that prepend the result AGAIN
80 listing = subprocess.run(
81 ["git", "ls-files", "--", f"{src}/*.c", f"{src}/*.cc"],
82 capture_output=True, text=True, cwd=src.parent if src.name == "src" else None,
83 )
84 files = [Path(p) for p in listing.stdout.split() if p and "/external/" not in p]
85 if not files: # not a git checkout: fall back to a walk
86 files = [p for p in src.rglob("*.c") if "external" not in p.parts]
87 files += [p for p in src.rglob("*.cc") if "external" not in p.parts]
88
89 findings = []
90 for path in files:
91 if "database" in path.parts:
92 continue
93 try:
94 text = path.read_text(encoding="utf-8", errors="replace")
95 except OSError:
96 continue
97 if not any(name in text for name in unreversed):
98 continue
99 for caller, body in _functions(text):
100 if "g_list_prepend" not in body:
101 continue
102 for name in unreversed:
103 if name + "(" in body:
104 findings.append((name, unreversed[name], path, caller))
105
106 if not findings:
107 print(f"OK: {len(unreversed)} unreversed list(s) leave src/database, none double-prepended.")
108 return 0
109
110 print("FAILED: a repository list is prepended twice -- its order is flipped vs the "
111 "single-loop original.\n")
112 for name, defined_in, path, caller in findings:
113 print(f" {name}() [{defined_in}]")
114 print(f" prepended again by {path}:{caller}()")
115 print("\nExactly one of the two must reverse. Reverse in the REPOSITORY (return row order) "
116 "\nand say why in a comment beside the return.")
117 return 1
118
119
120if __name__ == "__main__":
121 sys.exit(main(sys.argv))
int main()
Definition prova.c:47