Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
forward_decl_audit.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Find forward declarations that let a header reach a type from a HIGHER layer.
3
4A forward declaration is normally a good thing: `struct dt_foo_t;` lets a header mention a
5type without including the header that defines it, which is how include graphs stay small.
6But it also silently defeats the layering check, because there is no #include to count.
7
8`pixel/format.h` was the case that prompted this: it forward-declared `dt_iop_module_t`,
9`dt_dev_pixelpipe_t` and `dt_dev_pixelpipe_iop_t` -- three develop/ types, two layers up --
10purely so a layer-2 header could declare three functions over them. `tools/include_graph.py`
11saw nothing, because nothing was included.
12
13This reports every forward declaration whose type is really defined at a higher layer. Most
14findings are legitimate opaque handles: passing a pointer through without touching its
15fields costs nothing and creates no real dependency. The ones to look at are where the header
16also *declares functions* over the type -- that is the shape that turned out to be an API in
17the wrong place.
18
19Usage:
20 tools/forward_decl_audit.py [--all] [--json]
21
22--all also lists same-layer and downward forward declarations, which are never a problem.
23"""
24
25import collections
26import json
27import os
28import importlib.util
29import re
30import sys
31
32REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
33SRC = os.path.join(REPO, "src")
34
35# IMPORTED from tools/include_graph.py, not copied. This used to be a second copy carrying a
36# comment that said it was "kept in step deliberately: a second, disagreeing copy of the layer
37# order would be worse than none" -- and it had already drifted, missing caches/, database/,
38# metadata/ and history/ entirely and holding widgets/ at the layer it left. The comment was
39# right about the hazard and wrong that a copy could avoid it.
40_ig_spec = importlib.util.spec_from_file_location(
41 "_include_graph", os.path.join(os.path.dirname(os.path.abspath(__file__)), "include_graph.py"))
42_ig = importlib.util.module_from_spec(_ig_spec)
43_ig_spec.loader.exec_module(_ig) # module level is definitions only; main() is under __main__
44LAYERS = _ig.LAYERS
45LAYER = _ig.LAYER
46
47FWD = re.compile(r'^\s*(?:struct|union|enum)\s+([A-Za-z_]\w*)\s*;\s*$')
48# `} name;` closes a struct/union/enum definition; `struct name {` opens one.
49DEF_CLOSE = re.compile(r'^\s*\}\s*([A-Za-z_]\w*)\s*;')
50DEF_OPEN = re.compile(r'^\s*(?:typedef\s+)?(?:struct|union|enum)\s+([A-Za-z_]\w*)\s*\{')
51
52
53COMMENT_BLOCK = re.compile(r'/\*.*?\*/', re.S)
54COMMENT_LINE = re.compile(r'//[^\n]*')
55
56
57def count_mentions(path, name):
58 """Lines mentioning `name` outside comments and outside its own forward declaration."""
59 try:
60 text = open(path, errors="ignore").read()
61 except OSError:
62 return 0
63 text = COMMENT_BLOCK.sub("", text)
64 text = COMMENT_LINE.sub("", text)
65 pat = re.compile(r'\b' + re.escape(name) + r'\b')
66 return sum(1 for line in text.split("\n")
67 if pat.search(line) and not FWD.match(line))
68
69
70def layer_of(relpath):
71 parts = relpath.split(os.sep)
72 if len(parts) < 2:
73 return None
74 if len(parts) == 2:
75 return LAYER['app']
76 return LAYER.get(parts[1])
77
78
80 for dirpath, dirnames, filenames in os.walk(SRC):
81 dirnames[:] = [d for d in dirnames if d not in ("external", "build")]
82 for fn in filenames:
83 if fn.endswith((".h", ".hpp", ".c", ".cc", ".cpp")):
84 full = os.path.join(dirpath, fn)
85 yield os.path.relpath(full, REPO), full
86
87
88def main():
89 show_all = "--all" in sys.argv
90 as_json = "--json" in sys.argv
91
92 definitions = {} # type name -> relpath where it is defined
93 forwards = collections.defaultdict(list) # type name -> [(relpath, line)]
94
95 for rel, full in walk_sources():
96 try:
97 lines = open(full, errors="ignore").read().split("\n")
98 except OSError:
99 continue
100 is_header = rel.endswith((".h", ".hpp"))
101 for n, line in enumerate(lines, 1):
102 m = FWD.match(line)
103 if m and is_header:
104 forwards[m.group(1)].append((rel, n))
105 continue
106 for pat in (DEF_CLOSE, DEF_OPEN):
107 d = pat.match(line)
108 if d:
109 # First definition wins; headers are walked before their .c in practice,
110 # and a type defined twice is a different problem than this one.
111 definitions.setdefault(d.group(1), rel)
112
113 findings = []
114 for name, places in forwards.items():
115 home = definitions.get(name)
116 if not home:
117 continue # opaque everywhere, or defined outside src/
118 home_layer = layer_of(home)
119 if home_layer is None:
120 continue
121 for rel, line in places:
122 here = layer_of(rel)
123 if here is None or rel == home:
124 continue
125 upward = home_layer > here
126 if upward or show_all:
127 # Does this header also declare functions mentioning the type? That is what
128 # separates "opaque pointer passed through" from "API declared in the wrong
129 # place", and it is the difference worth acting on.
130 #
131 # Counted by mentions outside comments, NOT by matching a prototype: this
132 # codebase wraps prototypes across lines, and a single-line regex reported
133 # common/iop_profile.h as bare when it declares six functions over the type.
134 uses = count_mentions(os.path.join(REPO, rel), name)
135 findings.append({
136 "type": name,
137 "forward_declared_in": f"{rel}:{line}",
138 "declaring_layer": here,
139 "defined_in": home,
140 "defining_layer": home_layer,
141 "upward": upward,
142 "prototypes_using_it": uses,
143 })
144
145 if as_json:
146 print(json.dumps(findings, indent=2))
147 return 0
148
149 up = [f for f in findings if f["upward"]]
150 with_api = [f for f in up if f["prototypes_using_it"] > 0]
151 passthrough = [f for f in up if f["prototypes_using_it"] == 0]
152
153 print(f"{len(up)} forward declaration(s) reaching a higher layer "
154 f"({len(with_api)} with prototypes over the type, {len(passthrough)} bare)\n")
155
156 print("--- reaching UP and used in prototypes: an API that may be in the wrong place ---")
157 for f in sorted(with_api, key=lambda x: (-x["defining_layer"] + x["declaring_layer"],
158 x["forward_declared_in"])):
159 print(f" {f['type']} (layer {f['declaring_layer']} -> {f['defining_layer']}, "
160 f"{f['prototypes_using_it']} prototype(s))")
161 print(f" declared {f['forward_declared_in']}")
162 print(f" defined {f['defined_in']}")
163
164 print(f"\n--- reaching UP, bare (opaque handles; usually fine) : {len(passthrough)} ---")
165 by_pair = collections.Counter(
166 (f["forward_declared_in"].split(":")[0], f["defined_in"]) for f in passthrough)
167 for (h, d), n in by_pair.most_common(20):
168 print(f" {n:3d} {h} -> {d}")
169
170 return 0
171
172
173if __name__ == "__main__":
174 sys.exit(main())