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
23SRC = 'src'
24SKIP_DIRS = {'external'}
25PRAGMA_RE = re.compile(r'^[ \t]*#[ \t]*pragma[ \t]+once[ \t]*\r?\n', re.M)
26GUARD_RE = re.compile(r'^[ \t]*#[ \t]*ifndef[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]*\r?\n'
27 r'[ \t]*#[ \t]*define[ \t]+', re.M)
28
29# X-macro headers: deliberately re-included several times in the SAME translation unit
30# with different macros defined, and expanded inside struct bodies to generate members.
31# They must have NEITHER a guard NOR any #include of their own.
32XMACRO_HEADERS = {
33 'src/common/module_api.h',
34 'src/views/view_api.h',
35 'src/libs/lib_api.h',
36 'src/imageio/format/imageio_format_api.h',
37 'src/imageio/storage/imageio_storage_api.h',
38}
39
40# Trailing editor modelines are conventionally the last thing in these files; the
41# #endif has to go above them to stay inside the guarded region only if the file's
42# content does. Keeping the modeline block outside the guard is harmless and matches
43# how the hand-written guards in the tree already look.
44MODELINE = '// clang-format off\n// modelines:'
45
46
47def guard_name(path):
48 rel = os.path.relpath(path, SRC)
49 return 'DT_' + re.sub(r'[^A-Za-z0-9]', '_', rel).upper()
50
51
52def headers():
53 for root, dirs, names in os.walk(SRC):
54 dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
55 for n in names:
56 if n.endswith(('.h', '.hpp')):
57 yield os.path.join(root, n)
58
59
60def convert(text, guard):
61 m = PRAGMA_RE.search(text)
62 if not m:
63 return None
64 # Count DIRECTIVES, not substrings: several headers legitimately mention
65 # "#pragma once" inside an explanatory comment.
66 if len(PRAGMA_RE.findall(text)) > 1:
67 raise ValueError('more than one #pragma once directive')
68
69 head = text[:m.start()]
70 tail = text[m.end():]
71 opening = '#ifndef %s\n#define %s\n' % (guard, guard)
72
73 idx = tail.rfind(MODELINE)
74 if idx == -1:
75 closing = '\n#endif // %s\n' % guard
76 return head + opening + tail.rstrip('\n') + closing
77 # place #endif just above the trailing modeline block
78 body, trailer = tail[:idx], tail[idx:]
79 return head + opening + body.rstrip('\n') + '\n\n#endif // %s\n\n' % guard + trailer
80
81
82def wrap_unguarded(text, guard):
83 """Guard a header that has neither #pragma once nor an #ifndef/#define pair.
84
85 The opening goes after the leading licence block comment (so the guard wraps the
86 actual content, not the whole file including its header comment); the #endif goes
87 just above the trailing modeline block, mirroring convert().
88 """
89 start = 0
90 stripped = text.lstrip()
91 if stripped.startswith('/*'):
92 end = text.find('*/')
93 if end != -1:
94 start = text.index('\n', end) + 1 if '\n' in text[end:] else len(text)
95
96 opening = '\n#ifndef %s\n#define %s\n' % (guard, guard)
97 head, rest = text[:start], text[start:]
98
99 idx = rest.rfind(MODELINE)
100 if idx == -1:
101 return head + opening + rest.rstrip('\n') + '\n\n#endif // %s\n' % guard
102 body, trailer = rest[:idx], rest[idx:]
103 return head + opening + body.rstrip('\n') + '\n\n#endif // %s\n\n' % guard + trailer
104
105
106def main():
107 check = '--check' in sys.argv
108 add_missing = '--add-missing' in sys.argv
109
110 if '--verify' in sys.argv:
111 offenders = [p for p in sorted(headers())
112 if PRAGMA_RE.search(open(p, encoding='utf-8').read())]
113 for p in offenders:
114 print('%s: #pragma once is forbidden, use an include guard' % p, file=sys.stderr)
115 return 1 if offenders else 0
116
117 changed = skipped = 0
118 seen = {}
119 for p in sorted(headers()):
120 text = open(p, encoding='utf-8').read()
121 if '#pragma once' not in text:
122 if not add_missing or p.replace(os.sep, '/') in XMACRO_HEADERS:
123 continue
124 if GUARD_RE.search(text):
125 continue
126 g = guard_name(p)
127 changed += 1
128 if check:
129 print('%s -> %s (was UNGUARDED)' % (p, g))
130 else:
131 open(p, 'w', encoding='utf-8').write(wrap_unguarded(text, g))
132 continue
133 g = guard_name(p)
134 if g in seen:
135 print('COLLISION: %s and %s both map to %s' % (p, seen[g], g), file=sys.stderr)
136 return 1
137 seen[g] = p
138 try:
139 out = convert(text, g)
140 except ValueError as e:
141 print('SKIP %s: %s' % (p, e), file=sys.stderr)
142 skipped += 1
143 continue
144 if out is None:
145 continue
146 changed += 1
147 if check:
148 print('%s -> %s' % (p, g))
149 else:
150 open(p, 'w', encoding='utf-8').write(out)
151 print('%s %d headers (%d skipped)' % ('would convert' if check else 'converted', changed, skipped))
152 return 0
153
154
155if __name__ == '__main__':
156 sys.exit(main())