Ansel
0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
header_consumers.py
Go to the documentation of this file.
1
#!/usr/bin/env python3
2
"""Report what each includer of a header actually consumes through it.
3
4
`gui/gtk.h` is included by 140 files, but almost none of them want 140 files' worth of
5
header. Most want two or three symbols; a good number want *nothing it declares* and are
6
only there for what it drags in transitively (that is how an earlier split attempt broke
7
`control/control.h`, which was getting `dt_control_t` through this chain and lost it when
8
the chain was shortened).
9
10
Splitting a god-header safely needs that distinction made explicit per file, so this
11
separates three cases for every includer:
12
13
OWN - uses a symbol the header itself declares/defines. Needs whichever new header
14
that symbol lands in.
15
VIA - uses no own symbol, but uses a symbol from a header this one includes. The
16
include is a transitive supply line; the file needs that header from somewhere
17
else. Only headers it cannot already reach through its *other* includes are
18
reported, so the list is what actually has to be added, not everything it happens
19
to touch.
20
UNUSED - uses nothing from the header or its transitive closure. The include can go.
21
22
Symbols are collected per header (functions, macros, types, enums, struct tags) and matched
23
against each includer by word-boundary search outside comments and strings. That over-counts
24
slightly -- a name mentioned in a comment-like context, or a symbol also reachable from a
25
different header -- so treat VIA as "candidate direct include", not gospel.
26
27
Usage:
28
tools/header_consumers.py gui/gtk.h [--json] [--only own|via|unused]
29
"""
30
31
import
collections
32
import
json
33
import
os
34
import
re
35
import
sys
36
37
REPO =
os.path.dirname
(
os.path.dirname
(
os.path.abspath
(__file__)))
38
SRC =
os.path.join
(REPO,
"src"
)
39
40
COMMENT_BLOCK =
re.compile
(
r"/\*.*?\*/"
,
re.S
)
41
COMMENT_LINE =
re.compile
(
r"//[^\n]*"
)
42
STRING_LIT =
re.compile
(
r'"(?:\\.|[^"\\])*"'
)
43
44
INCLUDE =
re.compile
(
r'^\s*#\s*include\s+"([^"]+)"'
)
45
46
# What counts as a symbol this header supplies.
47
DECLARE_PATTERNS = [
48
re.compile
(
r"^\s*#\s*define\s+([A-Za-z_]\w*)"
),
49
re.compile
(
r"^\s*}[^;]*?\b([A-Za-z_]\w*)\s*;"
),
# `} name;` and `} ATTR(..) name;`
50
re.compile
(
r"^\s*typedef\s+.*?\b([A-Za-z_]\w*)\s*;"
),
51
re.compile
(
r"^\s*typedef\s+.*\(\s*\*\s*([A-Za-z_]\w*)\s*\)\s*\("
),
# function-pointer typedef # typedef one-liner
52
re.compile
(
r"^\s*(?:struct|union|enum)\s+([A-Za-z_]\w*)\s*[;{]"
),
53
# A declaration or definition at file scope: <type stuff> name(
54
re.compile
(
r"^\s*(?:[A-Za-z_][\w \t*]*?[ \t*])([A-Za-z_]\w*)\s*\("
),
55
]
56
57
# Enum members are supplied too, and they are how DT_GUI_COLOR_* / DT_UI_CONTAINER_* travel.
58
ENUM_MEMBER =
re.compile
(
r"^\s*(DT_[A-Z0-9_]+)\s*(?:=|,|$)"
)
59
60
# Keywords and primitive types get matched by the declaration patterns (a `void foo(` line
61
# yields both) and by every consumer, so they would attribute every file to every header.
62
NOISE = {
63
"if"
,
"for"
,
"while"
,
"switch"
,
"return"
,
"sizeof"
,
"defined"
,
"else"
,
"do"
,
64
"static"
,
"inline"
,
"const"
,
"struct"
,
"union"
,
"enum"
,
"typedef"
,
"extern"
,
65
"void"
,
"int"
,
"char"
,
"float"
,
"double"
,
"long"
,
"short"
,
"unsigned"
,
"signed"
,
66
"gboolean"
,
"gint"
,
"guint"
,
"gchar"
,
"gpointer"
,
"gdouble"
,
"gfloat"
,
"gsize"
,
67
"size_t"
,
"ssize_t"
,
"uint8_t"
,
"uint16_t"
,
"uint32_t"
,
"uint64_t"
,
68
"int8_t"
,
"int16_t"
,
"int32_t"
,
"int64_t"
,
"va_list"
,
"FILE"
,
69
"TRUE"
,
"FALSE"
,
"NULL"
,
70
}
71
72
73
def
strip_noise
(text):
74
"""Blank comments and string literals, in ONE pass.
75
76
Three regexes applied in sequence cannot do this, in either order, because each construct
77
can contain the others' delimiters. Comments-first blanks from the `//` of a URL to the end
78
of the line, taking the string's closing quote with it; the surviving opening quote then
79
pairs with the next quote further down the file and swallows everything between. Measured on
80
src/gui/actions/help.c: 7550 of 9491 characters gone, and its three dt_control_log() calls
81
with them -- the file was reported as using NOTHING from control.h while calling it three
82
times. 129 files in this tree contain `//` inside a string literal.
83
84
Strings-first fails symmetrically: a lone `"` inside a comment (`// don't use "foo.h" here`
85
is fine, an unbalanced one is not) starts a literal that eats real code.
86
87
So: scan once, tracking which construct we are inside. Newlines are preserved so line
88
numbers stay meaningful; everything else becomes a space, and a string literal becomes the
89
empty pair the callers already expect.
90
"""
91
out = []
92
i, n = 0, len(text)
93
while
i < n:
94
c = text[i]
95
if
c ==
'/'
and
i + 1 < n
and
text[i + 1] ==
'*'
:
96
j =
text.find
(
'*/'
, i + 2)
97
j = n
if
j < 0
else
j + 2
98
out.append
(
""
.
join
(ch
if
ch ==
'\n'
else
' '
for
ch
in
text[i:j]))
99
i = j
100
elif
c ==
'/'
and
i + 1 < n
and
text[i + 1] ==
'/'
:
101
j =
text.find
(
'\n'
, i)
102
j = n
if
j < 0
else
j
103
out.append
(
' '
* (j - i))
104
i = j
105
elif
c ==
'"'
or
c ==
"'"
:
106
quote, j = c, i + 1
107
while
j < n:
108
if
text[j] ==
'\\'
:
109
j += 2
110
continue
111
if
text[j] == quote
or
text[j] ==
'\n'
:
# newline: unterminated, stop there
112
break
113
j += 1
114
if
j < n
and
text[j] == quote:
115
j += 1
116
out.append
(
'""'
if
quote ==
'"'
else
"''"
)
117
out.append
(
""
.
join
(ch
if
ch ==
'\n'
else
''
for
ch
in
text[i:j]))
118
i = j
119
else
:
120
out.append
(c)
121
i += 1
122
return
""
.
join
(out)
123
124
125
def
read
(path):
126
try
:
127
with
open
(path, errors=
"ignore"
)
as
fh:
128
return
fh.read
()
129
except
OSError:
130
return
""
131
132
133
def
symbols_of
(path):
134
"""Every identifier `path` supplies to whoever includes it."""
135
text =
strip_noise
(
read
(path))
136
found = set()
137
for
line
in
text.split
(
"\n"
):
138
for
pat
in
DECLARE_PATTERNS:
139
m =
pat.match
(line)
140
if
m:
141
found.add
(
m.group
(1))
142
m =
ENUM_MEMBER.match
(line)
143
if
m:
144
found.add
(
m.group
(1))
145
return
{s
for
s
in
found
if
s
not
in
NOISE
and
len(s) > 2}
146
147
148
def
resolve
(inc, from_dir):
149
"""An include is written either relative to src/ or to the including file's directory."""
150
for
cand
in
(
os.path.join
(SRC, inc),
os.path.join
(from_dir, inc)):
151
cand =
os.path.normpath
(cand)
152
if
os.path.isfile
(cand):
153
return
cand
154
return
None
155
156
157
def
closure
(header, seen=None):
158
"""Headers reachable from `header`, excluding itself."""
159
if
seen
is
None
:
160
seen = set()
161
for
line
in
read
(header).
split
(
"\n"
):
162
m =
INCLUDE.match
(line)
163
if
not
m:
164
continue
165
target =
resolve
(
m.group
(1),
os.path.dirname
(header))
166
if
target
and
target
not
in
seen:
167
seen.add
(target)
168
closure
(target, seen)
169
return
seen
170
171
172
def
walk_sources
():
173
for
dirpath, dirnames, filenames
in
os.walk
(SRC):
174
dirnames[:] = [d
for
d
in
dirnames
if
d
not
in
(
"external"
,
"build"
)]
175
for
fn
in
filenames:
176
if
fn.endswith
((
".h"
,
".hpp"
,
".c"
,
".cc"
,
".cpp"
)):
177
yield
os.path.join
(dirpath, fn)
178
179
180
def
main
():
181
args = [a
for
a
in
sys.argv
[1:]
if
not
a.startswith
(
"--"
)]
182
if
not
args:
183
sys.exit
(__doc__)
184
rel = args[0]
185
as_json =
"--json"
in
sys.argv
186
only =
None
187
if
"--only"
in
sys.argv
:
188
only =
sys.argv
[
sys.argv.index
(
"--only"
) + 1]
189
190
header =
os.path.join
(SRC, rel)
191
if
not
os.path.isfile
(header):
192
sys.exit
(f
"error: no such header: {header}"
)
193
194
own =
symbols_of
(header)
195
# Which included header supplies which symbol, so a VIA finding names its replacement.
196
supplier = {}
197
for
dep
in
closure
(header):
198
for
sym
in
symbols_of
(dep) - own:
199
supplier.setdefault
(sym,
os.path.relpath
(dep, SRC))
200
201
basename =
os.path.basename
(rel)
202
# Both spellings: a project header included as <gui/gtk.h> counts exactly the same, and
203
# two headers in this tree are written that way.
204
include_re =
re.compile
(
205
r'^\s*#\s*include\s+["<](?:.*/)?'
+
re.escape
(basename) +
r'[">]'
,
re.M
)
206
207
rows = []
208
for
path
in
walk_sources
():
209
if
os.path.samefile
(path, header):
210
continue
211
raw =
read
(path)
212
if
not
include_re.search
(raw):
213
continue
214
body =
strip_noise
(
include_re.sub
(
" "
, raw))
215
words = set(
re.findall
(
r"\b[A-Za-z_]\w*\b"
, body))
216
217
# What this file can already reach without the header under audit. A symbol available
218
# through one of its own other includes is not something it needs to add.
219
# Scanned on the raw text: strip_noise() blanks string literals, which is where an
220
# include path lives.
221
already = set()
222
for
line
in
include_re.sub
(
" "
, raw).
split
(
"\n"
):
223
m =
INCLUDE.match
(line)
224
if
not
m:
225
continue
226
target =
resolve
(
m.group
(1),
os.path.dirname
(path))
227
if
target:
228
already.add
(target)
229
already |=
closure
(target)
230
already = {
os.path.relpath
(h, SRC)
for
h
in
already}
231
232
used_own = sorted(own & words)
233
used_via =
collections.defaultdict
(list)
234
for
sym
in
words &
supplier.keys
():
235
if
supplier[sym]
in
already:
236
continue
237
used_via[supplier[sym]].
append
(sym)
238
239
kind =
"own"
if
used_own
else
(
"via"
if
used_via
else
"unused"
)
240
rows.append
({
241
"file"
:
os.path.relpath
(path, REPO),
242
"kind"
: kind,
243
"own_symbols"
: used_own,
244
"via"
: {k: sorted(v)
for
k, v
in
sorted(
used_via.items
())},
245
})
246
247
if
only:
248
rows = [r
for
r
in
rows
if
r[
"kind"
] == only]
249
250
if
as_json:
251
print(
json.dumps
(rows, indent=2))
252
return
0
253
254
counts =
collections.Counter
(r[
"kind"
]
for
r
in
rows)
255
print(f
"{len(rows)} file(s) include {rel}: "
256
f
"{counts['own']} use its own symbols, {counts['via']} only pull others through it, "
257
f
"{counts['unused']} use nothing\n"
)
258
259
# Which of this header's own symbols are actually wanted, and by how many files. This is
260
# the split plan: symbols nobody uses can just go, and symbols used together tend to
261
# belong in the same new header.
262
demand =
collections.Counter
()
263
for
r
in
rows:
264
demand.update
(r[
"own_symbols"
])
265
print(f
"--- own symbols in demand ({len(demand)} of {len(own)} declared) ---"
)
266
for
sym, n
in
demand.most_common
():
267
print(f
" {n:4d} {sym}"
)
268
unwanted = sorted(own - set(demand))
269
print(f
"\n--- declared but used by no includer ({len(unwanted)}) ---"
)
270
print(
" "
+
", "
.
join
(unwanted)
if
unwanted
else
" (none)"
)
271
272
print(f
"\n--- files that only pull other headers through it ({counts['via']}) ---"
)
273
for
r
in
sorted(rows, key=
lambda
x: x[
"file"
]):
274
if
r[
"kind"
] !=
"via"
:
275
continue
276
print(f
" {r['file']}"
)
277
for
hdr, syms
in
r[
"via"
].
items
():
278
print(f
" {hdr}: {', '.join(syms[:6])}"
279
+ (
" ..."
if
len(syms) > 6
else
""
))
280
281
print(f
"\n--- files using nothing from it ({counts['unused']}) ---"
)
282
for
r
in
sorted(rows, key=
lambda
x: x[
"file"
]):
283
if
r[
"kind"
] ==
"unused"
:
284
print(f
" {r['file']}"
)
285
286
return
0
287
288
289
if
__name__ ==
"__main__"
:
290
sys.exit
(
main
())
L
const float L
Definition
colorspaces_inline_conversions.h:494
items
const dt_collection_sort_t items[]
Definition
filter.c:102
header_consumers.symbols_of
symbols_of(path)
Definition
header_consumers.py:133
header_consumers.resolve
resolve(inc, from_dir)
Definition
header_consumers.py:148
header_consumers.walk_sources
walk_sources()
Definition
header_consumers.py:172
header_consumers.read
read(path)
Definition
header_consumers.py:125
header_consumers.strip_noise
strip_noise(text)
Definition
header_consumers.py:73
header_consumers.closure
closure(header, seen=None)
Definition
header_consumers.py:157
header_consumers.main
main()
Definition
header_consumers.py:180
tools
header_consumers.py
Generated by
1.9.8