100 index, low, on, stack, out, counter = {}, {}, set(), [], [], [0]
106 index[node] = low[node] = counter[0]; counter[0] += 1
107 stack.append(node); on.add(node)
109 succs = list(graph.get(node, ()))
110 for i
in range(pi, len(succs)):
113 work[-1] = (node, i + 1); work.append((w, 0)); recurse =
True;
break
115 low[node] =
min(low[node], index[w])
118 if low[node] == index[node]:
121 w = stack.pop(); on.discard(w); comp.append(w)
127 low[parent] =
min(low[parent], low[node])
136 graph = defaultdict(set)
137 for p, text
in files.items():
138 for inc
in INCLUDE_RE.findall(text):
143 headers = [f
for f
in files
if f.endswith((
'.h',
'.hpp'))]
145 print(f
"nodes: {len(files)} ({len(headers)} headers) direct edges: {sum(len(v) for v in graph.values())}\n")
148 comps = [c
for c
in tarjan(graph, list(files))
if len(c) > 1]
149 print(f
"=== 1. INCLUDE CYCLES: {len(comps)} ===")
150 for c
in sorted(comps, key=len, reverse=
True)[:10]:
151 print(f
" cycle of {len(c)}:")
152 for f
in sorted(c)[:8]:
154 if len(c) > 8: print(f
" ... +{len(c)-8}")
156 print(
" none — the include graph is a DAG")
159 print(
"\n=== 2. LAYERING VIOLATIONS (a file including something from a HIGHER layer) ===")
160 viol = defaultdict(list)
161 for a, targets
in graph.items():
163 if la
is None:
continue
166 if lb
is None:
continue
168 viol[(a.split(os.sep)[1], b.split(os.sep)[1])].append((a, b))
169 for (da, db), items
in sorted(viol.items(), key=
lambda kv: -len(kv[1])):
170 print(f
" {da}/ -> {db}/ : {len(items)}")
171 for a, b
in items[:3]:
172 print(f
" {a} -> {b}")
178 def closure(n, seen=None):
179 if n
in memo:
return memo[n]
180 out, stack = set(), [n]
183 for w
in graph.get(cur, ()):
185 out.add(w); stack.append(w)
189 fan_in = defaultdict(int)
193 print(
"\n=== 3. GOD-HEADERS by transitive fan-in (TUs+headers that pull it in) ===")
194 for h, n
in sorted(fan_in.items(), key=
lambda kv: -kv[1])[:15]:
195 if h.endswith((
'.h',
'.hpp')):
196 print(f
" {n:5d} {h} (drags in {len(closure(h))} project headers)")
198 print(
"\n=== 4. HEAVIEST HEADERS by transitive closure (what including it costs) ===")
199 for h
in sorted(headers, key=
lambda x: -len(closure(x)))[:12]:
200 print(f
" {len(closure(h)):5d} {h}")
202 if '--summary' in sys.argv:
203 print(
"\n=== SUMMARY ===")
204 summary(files, graph, headers, closure, comps, viol)
205 if '--what-if' in sys.argv:
207 if '--mermaid' in sys.argv:
208 print(
"\n=== DIRECTORY GRAPH (dotted = layering inversion) ===")
209 mermaid(graph, set(viol.keys()))
214 """Re-count layering violations as if some files lived elsewhere.
216 Relocation is cheap to do and expensive to undo, and intuition is unreliable here:
217 moving the history/* cluster into develop/ LOOKS obviously right (it calls
218 dt_dev_* constantly) and measures at +15 violations, because its own consumers sit
219 below develop/. Simulate first.
221 Usage: --what-if src/common/foo.c=develop src/common/foo.h=develop
225 if a.startswith(
'src/')
and '=' in a:
226 src, dst = a.split(
'=', 1)
227 moves[os.path.normpath(src)] = dst
229 print(
' pass moves as src/path/file.c=destdir')
233 p = os.path.normpath(p)
236 parts = p.split(os.sep)
237 return parts[1]
if len(parts) > 1
else None
241 for a, targets
in graph.items():
242 la = LAYER.get(home(a, mv))
246 lb = LAYER.get(home(b, mv))
247 if lb
is not None and lb > la:
253 print(
'\n=== WHAT-IF ===')
254 for s_, d
in moves.items():
255 print(
' %s -> %s/' % (s_, d))
256 print(
' layering violations %d -> %d (%+d)' % (base, after, after - base))
259def summary(files, graph, headers, closure, comps, viol):
260 """One-line-per-metric output, for before/after comparison."""
261 orphan_headers = [h
for h
in headers
if not any(h
in graph.get(f, ())
for f
in files)]
262 print(f
"nodes\t{len(files)}")
263 print(f
"headers\t{len(headers)}")
264 print(f
"direct_edges\t{sum(len(v) for v in graph.values())}")
265 print(f
"cycles\t{len(comps)}")
266 print(f
"cycle_nodes\t{sum(len(c) for c in comps)}")
267 print(f
"layering_violations\t{sum(len(v) for v in viol.values())}")
268 print(f
"max_closure\t{max((len(closure(h)) for h in headers), default=0)}")
269 print(f
"mean_closure\t{sum(len(closure(h)) for h in headers) / max(len(headers), 1):.1f}")
270 tot = sum(len(closure(f))
for f
in files)
271 print(f
"total_transitive_edges\t{tot}")
277 tus = [f
for f
in files
if f.endswith((
'.c',
'.cc',
'.cpp'))]
278 tu_costs = sorted(len(closure(f))
for f
in tus)
280 print(f
"tus\t{len(tus)}")
281 print(f
"tu_mean_closure\t{sum(tu_costs) / len(tu_costs):.1f}")
282 print(f
"tu_median_closure\t{tu_costs[len(tu_costs) // 2]}")
283 print(f
"tu_max_closure\t{tu_costs[-1]}")
291 lines[f] = sum(1
for _
in open(f, encoding=
'utf-8', errors=
'replace'))
294 tu_line_costs = sorted(sum(lines.get(h, 0)
for h
in closure(f))
for f
in tus)
296 print(f
"tu_mean_closure_lines\t{sum(tu_line_costs) // len(tu_line_costs)}")
297 print(f
"tu_median_closure_lines\t{tu_line_costs[len(tu_line_costs) // 2]}")
298 print(f
"tu_max_closure_lines\t{tu_line_costs[-1]}")
301 dt_h = os.path.join(SRC,
'darktable.h')
303 reach = sum(1
for f
in files
if dt_h
in closure(f))
304 direct = sum(1
for f
in files
if dt_h
in graph.get(f, ()))
305 print(f
"darktable_h_reach\t{reach}")
306 print(f
"darktable_h_direct_includers\t{direct}")
307 print(f
"darktable_h_closure\t{len(closure(dt_h))}")