Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
pragma_once_to_guards.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Replace `#pragma once` with explicit include guards across src/.
3
4`#pragma once` silently makes a cyclic include graph compile: a header that is
5re-entered mid-definition is simply skipped, leaving the first inclusion to finish
6with whatever it had. Explicit guards behave the same way at the preprocessor level,
7but they are greppable, portable, and -- crucially -- they make the anti-pattern
8visible in review instead of hiding it.
9
10Guard names are derived from the path relative to src/, e.g.
11 src/develop/masks/masks_history.h -> DT_DEVELOP_MASKS_MASKS_HISTORY_H
12
13Usage:
14 python3 tools/pragma_once_to_guards.py --check # list what would change, touch nothing
15 python3 tools/pragma_once_to_guards.py # rewrite in place
16 python3 tools/pragma_once_to_guards.py --add-missing # also guard headers that have NO guard
17 python3 tools/pragma_once_to_guards.py --verify # exit 1 if any #pragma once came back
18"""
19import os
20import re
21import sys
22
23# Resolved from the tool's own location, not from the working directory: CI runs the
24# gates from wherever the build left it, and a sweep that silently finds no header is
25# a gate that silently passes.
26ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
27SRC = os.path.join(ROOT, 'src')
28SKIP_DIRS = {'external'}
29# Every spelling a header can carry in this tree. The C++ ones are rare -- one file
30# as of this writing -- which is exactly why they have to be listed: a rule enforced
31# over `.h` alone is a rule a `.hh` walks straight through, and the extension is part
32# of the guard name, so no two spellings of one basename can collide.
33HEADER_SUFFIXES = ('.h', '.hh', '.hpp', '.hxx')
34PRAGMA_RE = re.compile(r'^[ \t]*#[ \t]*pragma[ \t]+once[ \t]*\r?\n', re.M)
35GUARD_RE = re.compile(r'^[ \t]*#[ \t]*ifndef[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]*\r?\n'
36 r'[ \t]*#[ \t]*define[ \t]+', re.M)
37
38# X-macro headers: deliberately re-included several times in the SAME translation unit
39# with different macros defined, and expanded inside struct bodies to generate members.
40# They must have NEITHER a guard NOR any #include of their own.
41XMACRO_HEADERS = {
42 'src/common/module_api.h',
43 'src/views/view_api.h',
44 'src/libs/lib_api.h',
45 'src/imageio/format/imageio_format_api.h',
46 'src/imageio/storage/imageio_storage_api.h',
47}
48
49# darktable.h has a TRIPWIRE, not a guard: it #errors on re-inclusion, because a second
50# inclusion in one translation unit means the header arrived through a path nobody
51# intended. Guarding it would absorb that silently -- and the guard's own #define would
52# collide with the macro the tripwire tests. It must never be guarded.
53TRIPWIRE_HEADERS = {
54 'src/darktable.h',
55}
56
57# Everything that must stay without an #ifndef guard, for its own reason.
58UNGUARDED_BY_DESIGN = XMACRO_HEADERS | TRIPWIRE_HEADERS
59
60# Trailing editor modelines are conventionally the last thing in these files; the
61# #endif has to go above them to stay inside the guarded region only if the file's
62# content does. Keeping the modeline block outside the guard is harmless and matches
63# how the hand-written guards in the tree already look.
64#
65# Two spellings open that block: most files start it with `// clang-format off`, the
66# oldest ones inherited from darktable start straight at `// modelines:`. Looking only
67# for the long one puts the #endif BELOW the block on exactly those files. Longest
68# marker first, so a file carrying both is split above the clang-format line rather
69# than inside the block.
70MODELINES = ('// clang-format off\n// modelines:', '// modelines:')
71
72
73def rel(path):
74 return os.path.relpath(path, ROOT).replace(os.sep, '/')
75
76
77def trailer_index(text):
78 """Where the trailing modeline block starts, or -1."""
79 for marker in MODELINES:
80 idx = text.rfind(marker)
81 if idx != -1:
82 return idx
83 return -1
84
85
86def guard_name(path):
87 from_src = os.path.relpath(path, SRC)
88 return 'DT_' + re.sub(r'[^A-Za-z0-9]', '_', from_src).upper()
89
90
91def headers():
92 for root, dirs, names in os.walk(SRC):
93 dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
94 for n in names:
95 if n.endswith(HEADER_SUFFIXES):
96 yield os.path.join(root, n)
97
98
99def convert(text, guard):
100 m = PRAGMA_RE.search(text)
101 if not m:
102 return None
103 # Count DIRECTIVES, not substrings: several headers legitimately mention
104 # "#pragma once" inside an explanatory comment.
105 if len(PRAGMA_RE.findall(text)) > 1:
106 raise ValueError('more than one #pragma once directive')
107
108 head = text[:m.start()]
109 tail = text[m.end():]
110 opening = '#ifndef %s\n#define %s\n' % (guard, guard)
111
112 idx = trailer_index(tail)
113 if idx == -1:
114 closing = '\n#endif // %s\n' % guard
115 return head + opening + tail.rstrip('\n') + closing
116 # place #endif just above the trailing modeline block
117 body, trailer = tail[:idx], tail[idx:]
118 return head + opening + body.rstrip('\n') + '\n\n#endif // %s\n\n' % guard + trailer
119
120
121def wrap_unguarded(text, guard):
122 """Guard a header that has neither #pragma once nor an #ifndef/#define pair.
123
124 The opening goes after the leading licence block comment (so the guard wraps the
125 actual content, not the whole file including its header comment); the #endif goes
126 just above the trailing modeline block, mirroring convert().
127 """
128 start = 0
129 stripped = text.lstrip()
130 if stripped.startswith('/*'):
131 end = text.find('*/')
132 if end != -1:
133 start = text.index('\n', end) + 1 if '\n' in text[end:] else len(text)
134
135 opening = '\n#ifndef %s\n#define %s\n' % (guard, guard)
136 head, rest = text[:start], text[start:]
137
138 idx = trailer_index(rest)
139 if idx == -1:
140 return head + opening + rest.rstrip('\n') + '\n\n#endif // %s\n' % guard
141 body, trailer = rest[:idx], rest[idx:]
142 return head + opening + body.rstrip('\n') + '\n\n#endif // %s\n\n' % guard + trailer
143
144
145def main():
146 check = '--check' in sys.argv
147 add_missing = '--add-missing' in sys.argv
148
149 if '--verify' in sys.argv:
150 offenders = [p for p in sorted(headers())
151 if PRAGMA_RE.search(open(p, encoding='utf-8').read())]
152 for p in offenders:
153 print('%s: #pragma once is forbidden, use an include guard' % rel(p), file=sys.stderr)
154 return 1 if offenders else 0
155
156 changed = skipped = 0
157 seen = {}
158 for p in sorted(headers()):
159 text = open(p, encoding='utf-8').read()
160 if '#pragma once' not in text:
161 if not add_missing or rel(p) in UNGUARDED_BY_DESIGN:
162 continue
163 if GUARD_RE.search(text):
164 continue
165 g = guard_name(p)
166 changed += 1
167 if check:
168 print('%s -> %s (was UNGUARDED)' % (rel(p), g))
169 else:
170 open(p, 'w', encoding='utf-8').write(wrap_unguarded(text, g))
171 continue
172 g = guard_name(p)
173 if g in seen:
174 print('COLLISION: %s and %s both map to %s' % (rel(p), rel(seen[g]), g), file=sys.stderr)
175 return 1
176 seen[g] = p
177 try:
178 out = convert(text, g)
179 except ValueError as e:
180 print('SKIP %s: %s' % (rel(p), e), file=sys.stderr)
181 skipped += 1
182 continue
183 if out is None:
184 continue
185 changed += 1
186 if check:
187 print('%s -> %s' % (rel(p), g))
188 else:
189 open(p, 'w', encoding='utf-8').write(out)
190 print('%s %d headers (%d skipped)' % ('would convert' if check else 'converted', changed, skipped))
191 return 0
192
193
194if __name__ == '__main__':
195 sys.exit(main())