Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
fix_missing_includes.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Add the include that supplies each symbol the compiler says is missing.
3
4Removing a god-header leaves files that were quietly living off what it dragged in. The
5compiler names every one of them precisely -- `implicit declaration of function 'dt_print'`,
6`'DT_DEBUG_CONTROL' undeclared` -- so the repair is mechanical: find which header declares the
7symbol, add it, repeat until the build is clean.
8
9Reads a compiler log on stdin (or a file), extracts the missing symbols per file, resolves
10each against an index of every header under src/, and inserts the includes. A symbol declared
11in more than one header is resolved by preferring the lowest layer -- a leaf library over a
12module that re-exports it -- which is the include you want anyway.
13
14Usage:
15 ninja -k 0 2>&1 | tools/fix_missing_includes.py [--dry-run]
16"""
17
18import collections
19import os
20import re
21import sys
22
23REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
24SRC = os.path.join(REPO, "src")
25
26# Same order as tools/include_graph.py: lower is more fundamental, and a more fundamental
27# header is the better place to take a symbol from.
28LAYER = {
29 'external': 0, 'win': 0, 'system': 0,
30 'common': 1, 'math': 1, 'colorprofiles': 1,
31 'pixel': 2,
32 'control': 3,
33 'gui': 4, 'widgets': 4,
34 'develop': 5,
35 'iop': 6, 'imageio': 6,
36 'libs': 7, 'views': 7, 'chart': 7,
37 'apps': 10,
38}
39
40# Symbols that come from outside the tree. Without these the index guesses at whichever
41# project header happens to mention the name, which is how a layer-1 header gets told to
42# include control/jobs.h because it used a GList.
43EXTERNAL = [
44 (re.compile(r"^(?:GList|GSList|GHashTable|GError|GString|GArray|GPtrArray|GQueue|"
45 r"GValue|GObject|GType|GFile|GTimer|GThread|GMutex|GCond|GRegex|GKeyFile|"
46 r"GDateTime|GMainLoop|GMainContext|GSource|GBytes|GVariant|GCancellable|"
47 r"g_[a-z_]+|G_[A-Z_]+)$"), "<glib.h>"),
48 (re.compile(r"^(?:Gtk\w+|Gdk\w+|Pango\w+|gtk_\w+|gdk_\w+|pango_\w+|GTK_\w+|GDK_\w+|"
49 r"PANGO_\w+)$"), "<gtk/gtk.h>"),
50 (re.compile(r"^(?:cairo_\w+|CAIRO_\w+)$"), "<cairo.h>"),
51 (re.compile(r"^(?:u?int(?:8|16|32|64)_t|u?intptr_t|SIZE_MAX|(?:U?INT(?:8|16|32|64)_(?:MAX|MIN)))$"),
52 "<stdint.h>"),
53 (re.compile(r"^(?:_|N_|Q_|C_)$"), "<glib/gi18n.h>"),
54 (re.compile(r"^(?:printf|fprintf|snprintf|vsnprintf|fopen|fclose|fflush|stdout|stderr)$"),
55 "<stdio.h>"),
56 (re.compile(r"^(?:malloc|calloc|realloc|free|abs|qsort|getenv|exit)$"), "<stdlib.h>"),
57 (re.compile(r"^(?:memcpy|memset|strlen|strcmp|strncmp|strdup|strstr|strchr)$"), "<string.h>"),
58]
59
60
62 for pat, header in EXTERNAL:
63 if pat.match(sym):
64 return header
65 return None
66
67
68MISSING = [
69 re.compile(r"^(?P<file>[^:]+):\d+:\d+: (?:error|warning): implicit declaration of function '(?P<sym>\w+)'"),
70 re.compile(r"^(?P<file>[^:]+):\d+:\d+: error: '(?P<sym>\w+)' undeclared"),
71 re.compile(r"^(?P<file>[^:]+):\d+:\d+: error: unknown type name '(?P<sym>\w+)'"),
72 re.compile(r"^(?P<file>[^:]+):\d+:\d+: error: '(?P<sym>\w+)' was not declared in this scope"),
73]
74
75DECLARE = [
76 re.compile(r"^\s*#\s*define\s+([A-Za-z_]\w*)"),
77 re.compile(r"^\s*}[^;]*?\b([A-Za-z_]\w*)\s*;"), # `} name;` and `} ATTR(..) name;`
78 re.compile(r"^\s*typedef\s+.*?\b([A-Za-z_]\w*)\s*;"),
79 re.compile(r"^\s*typedef\s+.*\‍(\s*\*\s*([A-Za-z_]\w*)\s*\‍)\s*\‍("), # function-pointer typedef
80 re.compile(r"^\s*(?:struct|union|enum)\s+([A-Za-z_]\w*)\s*[;{]"),
81 re.compile(r"^\s*(?:[A-Za-z_][\w \t*]*?[ \t*])([A-Za-z_]\w*)\s*\‍("),
82]
83ENUM_MEMBER = re.compile(r"^\s*([A-Z][A-Z0-9_]{2,})\s*(?:=|,|$)")
84
85COMMENT_BLOCK = re.compile(r"/\*.*?\*/", re.S)
86COMMENT_LINE = re.compile(r"//[^\n]*")
87
88
89def layer_of(rel):
90 parts = rel.split(os.sep)
91 return LAYER.get(parts[0], 9) if len(parts) > 1 else 9
92
93
95 """symbol -> [header relpaths], best candidate first."""
96 index = collections.defaultdict(list)
97 for dirpath, dirnames, filenames in os.walk(SRC):
98 dirnames[:] = [d for d in dirnames if d not in ("external", "build")]
99 for fn in filenames:
100 if not fn.endswith((".h", ".hpp")) or fn.endswith(".cmake.h"):
101 continue
102 full = os.path.join(dirpath, fn)
103 rel = os.path.relpath(full, SRC)
104 try:
105 with open(full, errors="ignore") as fh:
106 text = fh.read()
107 except OSError:
108 continue
109 text = COMMENT_LINE.sub(" ", COMMENT_BLOCK.sub(" ", text))
110 for line in text.split("\n"):
111 for pat in DECLARE:
112 m = pat.match(line)
113 if m:
114 index[m.group(1)].append(rel)
115 m = ENUM_MEMBER.match(line)
116 if m:
117 index[m.group(1)].append(rel)
118 for sym, headers in index.items():
119 # Deduplicate, then prefer the lowest layer and, within a layer, the shortest path --
120 # `common/logging.h` over `common/some/deep/wrapper.h`.
121 seen = sorted(set(headers), key=lambda h: (layer_of(h), h.count(os.sep), len(h)))
122 index[sym] = seen
123 return index
124
125
126def _conditional_depth(text, offset):
127 """How many #if/#ifdef blocks enclose `offset`."""
128 depth = 0
129 for m in re.finditer(r'^\s*#\s*(if|ifdef|ifndef|endif)\b', text[:offset], re.M):
130 depth += -1 if m.group(1) == "endif" else 1
131 return max(0, depth)
132
133
134def insert_includes(path, headers):
135 with open(path) as fh:
136 text = fh.read()
137 existing = set(re.findall(r'^\s*#\s*include\s+["<]([^">]+)[">]', text, re.M))
138 todo = [h for h in headers if h.strip("<>") not in existing]
139 if not todo:
140 return []
141 block = "".join(f"#include {h}\n" if h.startswith("<") else f'#include "{h}"\n'
142 for h in sorted(todo))
143
144 # Insert into the file's *leading* include block. Anchoring on the last include anywhere
145 # is wrong: several files re-include a header mid-file on purpose (iop/iop_api.h is
146 # expanded twice in develop/imageop.c), and an include placed there is below every use.
147 first_code = len(text)
148 for m in re.finditer(r'^[A-Za-z_].*$', text, re.M):
149 line = m.group(0)
150 if line.startswith(("#", "//", "/*", "*")):
151 continue
152 if re.match(r'^(?:extern|G_BEGIN_DECLS|G_END_DECLS)\b', line):
153 continue
154 first_code = m.start()
155 break
156
157 # Only anchor on an include that is unconditionally compiled. Landing inside an
158 # `#ifdef GDK_WINDOWING_QUARTZ` block -- which is where the last include of several files
159 # sits -- means the include silently does nothing on every other platform, and the symbol
160 # stays missing with no new error to explain why.
161 anchors = [m for m in re.finditer(r'^\s*#\s*include\s+["<][^">]+[">].*\n', text, re.M)
162 if m.end() <= first_code and _conditional_depth(text, m.start()) == 0]
163 if anchors:
164 at = anchors[-1].end()
165 else:
166 guard = re.search(r'^\s*#\s*define\s+\w+_H\w*\s*\n', text, re.M)
167 at = guard.end() if guard else 0
168 with open(path, "w") as fh:
169 fh.write(text[:at] + block + text[at:])
170 return todo
171
172
173def main():
174 dry = "--dry-run" in sys.argv
175 log = sys.stdin.read()
176
177 wanted = collections.defaultdict(set)
178 for line in log.split("\n"):
179 line = line.replace(REPO + "/", "")
180 for pat in MISSING:
181 m = pat.match(line.strip())
182 if m:
183 wanted[m.group("file")].add(m.group("sym"))
184 break
185
186 if not wanted:
187 print("no missing symbols found in the log")
188 return 0
189
190 index = build_index()
191 unresolved = collections.Counter()
192 for path, syms in sorted(wanted.items()):
193 if not os.path.exists(path):
194 continue
195 self_header = os.path.splitext(path)[0] + ".h"
196 headers = []
197 for sym in sorted(syms):
198 ext = external_header(sym)
199 if ext:
200 headers.append(ext)
201 continue
202 cands = index.get(sym)
203 if not cands:
204 unresolved[sym] += 1
205 continue
206 # Never suggest the file's own header: if the symbol were there it would resolve.
207 pick = next((c for c in cands
208 if os.path.normpath(os.path.join(SRC, c)) != os.path.normpath(self_header)),
209 None)
210 if pick:
211 headers.append(pick)
212 headers = sorted(set(headers))
213 if not headers:
214 continue
215 if dry:
216 print(f"{path}: + {', '.join(headers)}")
217 else:
218 added = insert_includes(path, headers)
219 if added:
220 print(f"{path}: + {', '.join(added)}")
221
222 if unresolved:
223 print("\nunresolved symbols (no header declares them):", file=sys.stderr)
224 for sym, n in unresolved.most_common(20):
225 print(f" {n:3d} {sym}", file=sys.stderr)
226 return 0
227
228
229if __name__ == "__main__":
230 sys.exit(main())
const float max