2"""Build the "code health" panel published alongside the Doxygen API docs.
4The panel answers one question: how manageable is this codebase, in numbers that
5mean the same thing on darktable and on Ansel. It is generated identically in both
6repositories so the two published sites can be read side by side.
8Inputs, all optional except the first — a missing tool degrades its own section to
9"not available" instead of failing the build:
11 doc/api/sqlite3/doxygen_sqlite3.db Doxygen's own symbol table (GENERATE_SQLITE3),
12 produced by a fast first Doxygen pass. Gives
13 symbols per file and the include graph.
14 lizard cyclomatic complexity (CCN) per function.
15 cppcheck static analysis without needing a build.
16 <clang-tidy report>.json/.txt clang-tidy findings, when a separate job that
17 can produce compile_commands.json has run.
21 doc/code-health.md a Doxygen page (picked up by INPUT, themed, searchable)
22 doc/code-health.json the same numbers, machine-readable, for cross-repo diffing
25 python3 tools/code_health.py --project darktable --source-dir src \\
26 [--db doc/api/sqlite3/doxygen_sqlite3.db] [--clang-tidy-log FILE]
38from collections
import Counter, defaultdict
63 """Extend EXCLUDED_DIR_PARTS with every path declared in .gitmodules."""
67 with open(path, encoding=
"utf-8", errors=
"replace")
as fh:
81 if part
not in EXCLUDED_DIR_PARTS:
88 """Shell-glob form of the exclusion list, for tools that filter by pattern.
90 Derived on demand, never written out twice, so it always reflects the submodule
91 paths loaded from .gitmodules.
93 return tuple(
"*%s*" % part
for part
in EXCLUDED_DIR_PARTS)
106SOURCE_SUFFIXES = (
".c",
".cc",
".cpp",
".cxx",
".h",
".hpp",
".hxx",
".m",
".mm")
112 "C",
"C/C++ Header",
"C++",
"Objective-C",
"Objective-C++",
117 """True for a file that is compiled into the shipped application."""
122 """True for anything that must not be measured: vendored, dead, or not shipped."""
126 return any(part
in p
for part
in EXCLUDED_DIR_PARTS)
130 """Run a command, returning (ok, stdout). Never raises on a non-zero exit."""
133 errors=
"replace", check=
False, **kw)
136 return False,
str(exc)
143 """Symbols per file, from Doxygen's SQLite output.
145 memberdef.kind is Doxygen's own vocabulary: 'function', 'variable', 'typedef',
146 'macro definition', 'enumeration'. Note it is 'macro definition', not 'define'.
154 SELECT p.name AS path, m.kind AS kind, COUNT(*) AS n
155 FROM memberdef m JOIN path p ON p.rowid = m.file_id
156 GROUP BY p.name, m.kind
165 per_file = defaultdict(Counter)
166 for path, kind, n
in rows:
169 per_file[path][kind] += n
180 "macros":
kinds.get(
"macro definition", 0),
184 out.sort(key=
lambda r: (-r[
"total"], r[
"file"]))
189 """Every (including file, included file) pair inside this tree.
191 Doxygen's `includes` table is the same data its "included by" graphs are drawn
192 from, so the numbers derived here and the graphs on the file pages cannot drift
200 "SELECT name FROM sqlite_master WHERE type IN ('table','view')")}
201 if "includes" not in tables:
205 cols = {r[1]
for r
in con.execute(
"PRAGMA table_info(includes)")}
206 src =
"src_id" if "src_id" in cols
else (
"including_id" if "including_id" in cols
else None)
207 dst =
"dst_id" if "dst_id" in cols
else (
"included_id" if "included_id" in cols
else None)
208 if not src
or not dst:
214 "SELECT ps.name, pd.name FROM includes i "
215 "JOIN path ps ON ps.rowid = i.%s JOIN path pd ON pd.rowid = i.%s "
216 "WHERE ps.local = 1 AND pd.local = 1" % (src, dst)
228 """Build the include graph a second time, straight from the source text.
230 Doxygen only records an include it managed to RESOLVE, and resolution depends on
231 INCLUDE_PATH, on conditional compilation, and on which headers exist at doc-build
232 time. Measured on darktable 5.6 it missed 184 edges that plainly exist in the
233 files - platform headers behind #ifdef, mostly - while finding 133 the text does
234 not show, from generated headers. Each graph therefore contained a cyclic cluster
237 Neither is authoritative on its own, so the panel unions them. Resolution mirrors
238 the compiler: the including file's own directory first, then the source root.
244 known, files = set(), []
245 for dirpath, dirnames, filenames
in os.walk(src):
247 if any(part
in rel_dir
for part
in EXCLUDED_DIR_PARTS):
250 for name
in filenames:
257 for rel, full
in files:
259 with open(full, encoding=
"utf-8", errors=
"replace")
as fh:
276 """How many files include each header, directly.
278 This is the number behind the "included by" graphs: the fan-in of a header, and
279 the single clearest measure of how entangled a codebase's headers are.
284 for _including, included
in edges:
285 fan_in[included] += 1
290 """Transitive reach, in both directions. This is where a god header shows up.
292 Cycle counts cannot express the damage darktable.h does, and this was measured
293 before the section was written: darktable.h reaches only 14 files downstream, so
294 at most 14 could ever cycle with it and exactly 3 do. Its 4-file cluster is
295 correct and says almost nothing.
297 The number that matters is the other direction. 552 of 741 files reach it, so
298 three quarters of the codebase depends on that one header, transitively. Editing
299 it rebuilds and re-reviews nearly everything, and no cycle metric will say so.
301 dependents how many files end up depending on this header, directly or not.
302 High means expensive to change and hard to reason about.
303 depth how many headers a translation unit drags in transitively. High
304 means slow builds and a file whose real interface is unknowable
305 from its own include list.
309 succ, pred = defaultdict(set), defaultdict(set)
317 def closure(adj, start):
344 vfo = {n: len(closure(succ, n))
for n
in nodes}
345 vfi = {n: len(closure(pred, n))
for n
in nodes}
348 cyclic = defaultdict(list)
350 both = closure(succ, n) & closure(pred, n)
353 core_set =
max(cyclic, key=len)
if cyclic
else frozenset()
354 core_vfi =
max((vfi[n]
for n
in core_set), default=0)
355 core_vfo =
max((vfo[n]
for n
in core_set), default=0)
360 elif vfi[n] >= core_vfi
and vfo[n] < core_vfo:
361 buckets[
"shared"] += 1
362 elif vfi[n] < core_vfi
and vfo[n] >= core_vfo:
363 buckets[
"control"] += 1
365 buckets[
"peripheral"] += 1
367 headers = [n
for n
in nodes
if n.lower().
endswith((
".h",
".hpp",
".hxx"))]
370 n = len(closure(pred, h))
376 drags = len(closure(succ, h))
378 "share":
round(100.0 * n /
max(1, total), 1),
379 "drags_in": drags,
"burden": n * drags})
381 by_burden = sorted(dependents, key=
lambda r: -r[
"burden"])[:30]
383 sources = [n
for n
in nodes
if not n.lower().
endswith((
".h",
".hpp",
".hxx"))]
384 depth = [{
"file": c,
"headers_pulled": len(closure(succ, c))}
for c
in sources]
385 depth.sort(key=
lambda r: -r[
"headers_pulled"])
386 counts = sorted(r[
"headers_pulled"]
for r
in depth)
387 mean =
sum(counts) / float(len(counts))
if counts
else 0
391 "propagation_cost": propagation,
392 "core_size": len(core_set),
393 "core_share":
round(100.0 * len(core_set) /
max(1, total), 1),
394 "core_files": sorted(core_set)[:30],
395 "buckets":
dict(buckets),
396 "top_dependents": dependents[:30],
397 "top_burden": by_burden,
398 "headers_over_half":
sum(1
for r
in dependents
if r[
"share"] >= 50.0),
399 "headers_over_quarter":
sum(1
for r
in dependents
if r[
"share"] >= 25.0),
400 "deepest": depth[:20],
401 "mean_headers_pulled":
round(mean, 1),
402 "median_headers_pulled": counts[len(counts) // 2]
if counts
else 0,
403 "max_headers_pulled": counts[-1]
if counts
else 0,
408 """Newman modularity Q of one partition of a weighted undirected graph."""
411 inner, deg = Counter(), Counter()
416 if partition[v] == cu:
418 return sum(inner[c] / two_m - (deg[c] / two_m) ** 2
for c
in deg)
422 """Louvain community detection, first phase iterated to convergence.
424 Implemented here rather than pulled in, so the panel keeps needing nothing but
425 python3 on the runner. Only the local-moving phase is used - repeated to a fixed
426 point - which is enough to establish whether a better grouping than the directory
427 layout exists, without the graph-coarsening phase's bookkeeping.
429 partition = {n: n
for n
in adj_w}
430 comm_deg =
dict(degree)
431 for _
in range(passes):
433 for u
in sorted(adj_w):
437 for v, w
in adj_w[u].
items():
439 weights[partition[v]] += w
456 """Does the folder layout correspond to how the code is actually coupled?
458 The directories are treated as a proposed partition of the dependency graph and
459 scored with Newman modularity Q - the share of edges falling inside groups, minus
460 what random wiring of the same degrees would produce. Then a partition is derived
461 from the graph itself with Louvain and scored the same way.
463 The GAP between the two is the number that matters. If directories really were
464 modules, grouping by directory would be near-optimal and the gap would be small.
465 A large gap means the folders are drawers: the code clusters, but not along the
466 lines the tree is filed under.
468 Also reported without any clustering at all: the share of includes that stay
469 inside their own directory, which is the same question asked bluntly.
473 adj_w = defaultdict(Counter)
486 dirs[n] =
module_of(n, source_dir)
or "(root)"
489 derived =
_louvain(adj_w, degree, two_m)
490 q_derived =
_modularity(adj_w, degree, two_m, derived)
492 inside =
sum(1
for a, b
in edges
499 pair = defaultdict(Counter)
501 pair[c][dirs[n]] += 1
507 "q_directories":
round(q_dir, 3),
508 "q_derived":
round(q_derived, 3),
509 "gap":
round(q_derived - q_dir, 3),
511 "derived_clusters": len(clusters),
512 "largest_clusters": sizes[:10],
513 "intra_directory_includes": inside,
514 "total_includes": len(edges),
515 "intra_directory_share":
round(100.0 * inside /
max(1, len(edges)), 1),
516 "files_in_agreeing_cluster": agree,
518 "agreement_share":
round(100.0 * agree /
max(1, len(adj_w)), 1),
523 """Fold in a header self-containment report, when one has been produced.
525 A header should compile on its own. One that does not is relying on its includer
526 having pulled something in first, which is the same defect as an unnecessary
527 include seen from the other side: the dependency is real but written nowhere.
529 Testing it needs a compiler and the project's include flags, so it is produced by
530 the code-health workflow, which already configures a build tree, and consumed here.
535 with open(path, encoding=
"utf-8", errors=
"replace")
as fh:
537 except (OSError, ValueError)
as exc:
538 sys.stderr.write(
"code_health: self-containment report unreadable: %s\n" % exc)
540 failing = [f
for f
in data.get(
"results", [])
if not f.get(
"ok")]
541 total = len(
data.get(
"results", []))
544 "self_contained": total - len(failing),
545 "share":
round(100.0 * (total - len(failing)) /
max(1, total), 1),
546 "failing": sorted((f[
"header"],
f.get(
"first_error",
"")[:120])
for f
in failing)[:30],
547 "failing_count": len(failing),
552 """How much of the API carries any documentation at all.
554 Doxygen records a brief and a detailed description per symbol, so the tree's own
555 documentation coverage is a query rather than an estimate. Counted over the same
556 production files as everything else.
563 "SELECT p.name, m.kind, "
564 " TRIM(COALESCE(m.briefdescription,'')) || TRIM(COALESCE(m.detaileddescription,'')) "
565 "FROM memberdef m JOIN path p ON p.rowid = m.file_id"
573 total, documented = Counter(), Counter()
574 per_file = defaultdict(
lambda: [0, 0])
575 for path, kind, text
in rows:
579 per_file[path][0] += 1
580 if (text
or "").
strip():
581 documented[kind] += 1
582 per_file[path][1] += 1
587 by_kind = [{
"kind": k,
"symbols": total[k],
"documented": documented[k],
588 "share":
round(100.0 * documented[k] /
max(1, total[k]), 1)}
589 for k
in sorted(total, key=
lambda k: -total[k])]
590 undoc = sorted(((p, v[0] - v[1], v[0])
for p, v
in per_file.items()
if v[0] - v[1] > 0),
591 key=
lambda r: -r[1])[:25]
595 "share":
round(100.0 * d /
max(1, n), 1),
597 "worst_files": [{
"file": f,
"undocumented": u,
"symbols": t}
for f, u, t
in undoc],
601def collect_git(repo_root, days, per_file_ccn, max_files_per_commit=20):
602 """Evolution metrics: churn, hotspots, change coupling and ownership.
604 Process metrics predict defects better than static complexity does - complex code
605 nobody touches is harmless, complex code changed weekly is where the bugs are - and
606 none of the rest of this panel can see them, because they are not a property of the
607 code as it stands but of how it got there.
609 hotspot revisions x cyclomatic complexity. The prioritisation metric:
610 what to refactor first, rather than what is merely large.
611 change coupling files that keep changing together in the same commit. Some of
612 those pairs have no include edge between them at all, which is
613 coupling no static analysis can find.
614 ownership authors per file. Concentration is not automatically good or
615 bad - one author means fast decisions and a bus factor of one.
617 Commits touching more than max_files_per_commit production files are excluded from
618 the coupling counts only: a sweeping rename couples everything it touches to
619 everything else, which is an artefact of the commit rather than of the code. They
620 still count towards churn and ownership.
624 fmt =
"__COMMIT__%H\x1f%an"
625 ok, out =
run([
"git",
"-C", repo_root,
"log",
"--since=%d.days.ago" % days,
626 "--no-merges",
"--numstat",
"--format=" + fmt])
631 revisions, churn = Counter(), Counter()
632 authors = defaultdict(set)
635 current, author = [],
None
642 authors[f].add(author)
643 if len(current) <= max_files_per_commit:
644 uniq = sorted(set(current))
645 for i
in range(len(uniq)):
646 for j
in range(i + 1, len(uniq)):
647 cochange[(uniq[i], uniq[j])] += 1
654 _h, _sep, author = line[len(
"__COMMIT__"):].
partition(
"\x1f")
659 added, deleted, path = parts
663 churn[path] += int(added) + int(deleted)
676 "churn":
churn.get(f, 0),
"score": revs * cx})
681 ra, rb = revisions[a], revisions[b]
682 conf = 100.0 * n /
max(1,
min(ra, rb))
683 if n >= 5
and conf >= 40.0:
685 "confidence":
round(conf, 0)})
686 coupled.sort(key=
lambda r: (-r[
"together"], -r[
"confidence"]))
692 "files_touched": len(revisions),
694 "hotspots": hotspots[:25],
695 "coupled": coupled[:25],
696 "coupled_total": len(coupled),
697 "single_author_files":
sum(1
for c
in author_counts
if c == 1),
698 "mean_authors":
round(
sum(author_counts) / float(len(author_counts)), 2),
699 "max_authors": author_counts[-1]
if author_counts
else 0,
700 "most_revised": [{
"file": f,
"revisions": n,
"churn":
churn.get(f, 0)}
725 """The module a file belongs to: its first path component under the source dir."""
735 return parts[0]
if len(parts) > 1
else "(root)"
739 """Tarjan's SCC, iterative so a deep include chain cannot blow the stack."""
740 index, low, on_stack, stack, comps = {}, {}, set(), [], []
746 index[root] = low[root] = counter[0]
755 index[nxt] = low[nxt] = counter[0]
763 low[node] =
min(low[node], index[nxt])
768 low[work[-1][0]] =
min(low[work[-1][0]], low[node])
769 if low[node] == index[node]:
782 """Order nodes so that as few weighted edges as possible point backwards.
784 `edges` maps (a, b) -> weight, meaning "a depends on b". Returns (order, back),
785 where order[0] is the foundation and `back` lists the edges still pointing the
786 wrong way: the minimum set of dependencies that would have to go for a layering
787 to exist at all - a minimum feedback arc set.
789 Nothing is declared. Topologically sorting instead would measure nothing (a
790 topological order has no backward edges by construction) and does not exist
791 anyway once the graph has a cycle, which is the interesting case.
793 Eades-Lin-Smyth greedy: strip sinks to the back and sources to the front, and
794 when neither exists - exactly when a cycle is in the way - remove the node with
795 the largest outgoing-minus-incoming weight. Linear time, and at most
796 |E|/2 - |V|/6 backward edges. Implemented with worklists rather than rescans so
797 it stays linear on the file graph, which is two orders of magnitude larger than
804 succ, pred = defaultdict(list), defaultdict(list)
805 out_w, in_w = Counter(), Counter()
817 remaining = set(nodes)
818 sinks = [u
for u
in nodes
if out_w[u] == 0]
819 sources = [u
for u
in nodes
if in_w[u] == 0
and out_w[u] != 0]
827 if in_w[v] == 0
and out_w[v] != 0:
852 u =
max(remaining, key=
lambda m: (out_w[m] - in_w[m], m))
858 order = (head + tail[::-1])[::-1]
859 pos = {m: i
for i, m
in enumerate(order)}
863 back.append({
"pair":
"%s -> %s" % (a, b),
"includes": n,
864 "from_rank": pos[a],
"to_rank": pos[b]})
865 back.sort(key=
lambda v: (-v[
"includes"], v[
"pair"]))
870 """Summarise a feedback-arc-set ordering of one dependency graph."""
875 weighted =
sum(v[
"includes"]
for v
in back)
881 "order": [{
"rank": i,
"name": m}
for i, m
in enumerate(order)],
882 "back_edges": len(back),
883 "back_includes": weighted,
884 "back_ratio":
round(100.0 * weighted /
max(1, total), 1),
890 """Robert Martin's instability metric, and the violations it implies.
892 A second graph-derived view of the same question, independent of the ordering
893 above. Two independent derivations agreeing is worth more than either alone.
895 It is computed over directories, so it inherits their weakness as a unit - they
896 are drawers, not modules - and is reported for what it is:
898 Ca (afferent) how many modules depend on this one
899 Ce (efferent) how many modules this one depends on
900 I = Ce / (Ca + Ce) instability, 0 .. 1
902 I = 0 is a module everyone depends on and that depends on nothing: maximally
903 stable, expensive to change, and it had better be a leaf library. I = 1 is a
904 module nobody depends on: free to change, and it had better be a leaf consumer.
906 The Stable Dependencies Principle says a module should only depend on modules
907 at least as stable as itself. An edge A -> B with I(A) < I(B) breaks it: the
908 harder-to-change module was made to depend on the easier-to-change one, so the
909 volatile module's churn propagates into the stable one. That is the same defect
910 "layer inversion" is looking for, established without anyone declaring a layer.
912 Note the two can legitimately disagree, and where they do is interesting rather
913 than wrong: a widely used module that itself reaches into a volatile one scores
914 badly here even if the declared layers approve of it.
918 afferent, efferent = defaultdict(set), defaultdict(set)
919 for (a, b)
in mod_edges:
923 modules = sorted(set(afferent) | set(efferent))
926 ca, ce = len(afferent[m]), len(efferent[m])
927 inst[m] = (ce / float(ca + ce))
if (ca + ce)
else 0.0
929 violations, weighted, ranked = [], 0, 0
932 if inst[a] < inst[b] - 1e-9:
934 "from_I":
round(inst[a], 2),
"to_I":
round(inst[b], 2)})
938 table = [{
"module": m,
"Ca": len(afferent[m]),
"Ce": len(efferent[m]),
939 "I":
round(inst[m], 2)}
for m
in modules]
943 "violating_edges": len(violations),
944 "violating_includes": weighted,
945 "violation_ratio":
round(100.0 * weighted /
max(1, ranked), 1),
946 "worst": violations[:20],
951 """Dependency cycles and derived layering, at file and at directory level.
953 Everything here comes from the include graph. Nothing is declared.
955 Cycles are the objective part: if A depends on B and B on A, no layering of the
956 two exists, whatever anyone believes. Reported as strongly connected components.
958 The derived ordering is the graduated part: order the units so as few includes as
959 possible point backwards, and report what still does. Those edges are the minimum
960 set of dependencies that would have to go for a layering to exist at all.
962 The FILE graph is the primary unit, because it presumes nothing about how the
963 tree is organised - it does not need directories to be modules, which in
964 darktable they are not. The directory graph is reported too, as an aggregation
965 over what are really drawers rather than modules.
970 file_edges = Counter()
971 mod_edges = Counter()
972 file_succ = defaultdict(set)
978 file_edges[(a, b)] += 1
980 if ma
and mb
and ma != mb:
981 mod_edges[(ma, mb)] += 1
992 for comp
in file_cycles:
994 inner = {(a, b): 1
for a
in members
for b
in file_succ.get(a, ())
if b
in members}
998 "internal_edges": len(inner),
999 "cuts_to_break": len(back),
1000 "files": sorted(comp),
1001 "cut_edges": [v[
"pair"]
for v
in back],
1005 mod_succ = defaultdict(set)
1006 for (ma, mb)
in mod_edges:
1007 mod_succ[ma].add(mb)
1016 "module_edges": len(mod_edges),
1018 "module_cycles": mod_cycles[:15],
1019 "module_cycle_count": len(mod_cycles),
1020 "modules_in_cycles":
sum(len(c)
for c
in mod_cycles),
1021 "file_cycle_count": len(file_cycles),
1022 "file_cycles": cycle_detail,
1023 "largest_file_cycle_size":
max((len(c)
for c
in file_cycles), default=0),
1024 "file_cycle_cuts":
sum(c[
"cuts_to_break"]
for c
in cycle_detail),
1025 "files_in_cycles":
sum(len(c)
for c
in file_cycles),
1026 "largest_file_cycle": sorted(file_cycles[0])
if file_cycles
else [],
1031 """Who includes the application-global header.
1033 darktable's src/common/darktable.h and Ansel's src/darktable.h are the same file
1034 by descent. The number that matters is how many HEADERS include it: a .c doing so
1035 is a choice local to that file, a .h doing so pushes the whole application into
1036 every file downstream of it.
1047 headers = [a
for a, b
in edges
if b == target
and a.endswith((
".h",
".hpp"))]
1048 sources = [a
for a, b
in edges
if b == target
and not a.endswith((
".h",
".hpp"))]
1051 "included_by_headers": len(headers),
1052 "included_by_sources": len(sources),
1053 "total": len(headers) + len(sources),
1054 "headers": sorted(headers)[:40],
1062 """Per-function cyclomatic complexity, via lizard.
1064 The distribution matters more than the total: a codebase's maintenance cost lives
1065 in its tail, not its mean, so the thresholds below are reported as counts.
1069 cmd = [
"lizard",
"--csv",
"-l",
"c",
"-l",
"cpp"]
1090 nloc, ccn, _tok, params, length = (int(parts[i])
for i
in range(5))
1093 path, name = parts[6], parts[7]
1097 {
"file": path,
"name": name,
"ccn": ccn,
"nloc": nloc,
1098 "params": params,
"length": length}
1103 ccns = sorted(f[
"ccn"]
for f
in funcs)
1104 nlocs = [f[
"nloc"]
for f
in funcs]
1109 idx =
min(len(ccns) - 1,
max(0, int(
round((p / 100.0) * (len(ccns) - 1)))))
1112 per_file = Counter()
1114 per_file[f[
"file"]] += f[
"ccn"]
1115 worst = sorted(funcs, key=
lambda f: (-f[
"ccn"], -f[
"nloc"]))[:40]
1116 longest = sorted(funcs, key=
lambda f: -f[
"nloc"])[:20]
1118 "functions": len(funcs),
1119 "ccn_total":
sum(ccns),
1120 "ccn_mean":
round(
sum(ccns) / float(len(ccns)), 2),
1121 "ccn_median":
pct(50),
1124 "ccn_max": ccns[-1],
1125 "nloc_total":
sum(nlocs),
1126 "nloc_mean":
round(
sum(nlocs) / float(len(nlocs)), 1),
1127 "over_15":
sum(1
for c
in ccns
if c > 15),
1128 "over_25":
sum(1
for c
in ccns
if c > 25),
1129 "over_50":
sum(1
for c
in ccns
if c > 50),
1130 "over_100":
sum(1
for c
in ccns
if c > 100),
1131 "long_over_100_lines":
sum(1
for n
in nlocs
if n > 100),
1132 "long_over_300_lines":
sum(1
for n
in nlocs
if n > 300),
1133 "params_over_7":
sum(1
for f
in funcs
if f[
"params"] > 7),
1136 "per_file_ccn":
dict(per_file),
1144 """cppcheck findings by severity and by rule id.
1146 cppcheck is used rather than clang-tidy for the always-on panel because it needs
1147 no compile_commands.json, so it runs in the docs job on both repositories under
1148 identical conditions. clang-tidy findings, which need a configured build tree,
1149 are folded in from --clang-tidy-log when a job that can produce one has run.
1154 "cppcheck",
"--quiet",
"--enable=all",
"--inline-suppr",
1155 "--suppress=missingInclude",
"--suppress=missingIncludeSystem",
1156 "--suppress=unmatchedSuppression",
"--suppress=checkersReport",
1157 "--template={severity}|{id}|{file}",
1163 for part
in EXCLUDED_DIR_PARTS:
1167 cmd[-1:-1] = [
"-i", candidate]
1169 r =
subprocess.run(cmd, capture_output=
True, text=
True, errors=
"replace")
1174 by_sev, by_id = Counter(), Counter()
1179 sev, rule, path = parts[0], parts[1], parts[2]
1197 r"^(?P<file>[^:\s]+):\d+:\d+:\s+(?P<sev>warning|error):"
1198 r"\s+.*\[(?P<check>[a-zA-Z0-9_.\-,]+)\]\s*$"
1203 """Aggregate a clang-tidy run's console log by check name.
1205 Deliberately parses the log rather than running clang-tidy: producing
1206 compile_commands.json needs a configured build tree and the project's full
1207 dependency set, which does not belong in the docs job.
1211 by_check, by_sev, files = Counter(), Counter(), set()
1213 with open(log_path, encoding=
"utf-8", errors=
"replace")
as fh:
1230 if checks[0] ==
"clang-diagnostic-unknown-warning-option":
1236 by_check[checks[0]] += 1
1243 "files_with_findings": len(files),
1253 """Lines of code, counted per file and filtered with this module's own predicate.
1255 cloc's --not-match-d is NOT used to drop vendored code: depending on the cloc
1256 version it matches a single path component rather than a subtree, so
1257 src/external/rawspeed/src/... survives a --not-match-d on "external". That went
1258 unnoticed locally and inflated the published figures to 1,012,392 lines against
1259 the real 331,243. Counting --by-file and filtering through is_excluded() is the
1260 only way this section agrees with every other section of the panel.
1264 ok, out =
run([
"cloc",
"--quiet",
"--json",
"--by-file", source_dir])
1275 per_lang = defaultdict(Counter)
1277 lang =
v.get(
"language",
"unknown")
1282 if lang
not in PRODUCTION_LANGUAGES
or is_excluded(path):
1284 for key
in (
"blank",
"comment",
"code"):
1285 totals[key] +=
v.get(key, 0)
1286 per_lang[lang][key] +=
v.get(key, 0)
1287 totals[
"nFiles"] += 1
1288 per_lang[lang][
"nFiles"] += 1
1294 key=
lambda d: -
d.get(
"code", 0),
1296 return {
"sum":
dict(totals),
"languages": langs}
1304 return "_no data_\n"
1305 aligns = aligns
or [
"---"] * len(headers)
1306 out = [
"| " +
" | ".
join(headers) +
" |",
"| " +
" | ".
join(aligns) +
" |"]
1309 return "\n".
join(out) +
"\n"
1315 A(
"Code health {#code_health}")
1318 A(
"Generated with `tools/code_health.py` during the documentation build. The same")
1319 A(
"script, with the same thresholds and the same third-party exclusions, runs in the")
1320 A(
"Ansel repository and in the frozen darktable 5.0 reference tree, so the two")
1321 A(
"published panels can be read side by side.")
1323 A(
"Vendored code is excluded throughout (`src/external/`, the integration-test data,")
1324 A(
"the Doxygen theme), matching `sonar.exclusions` in `.sonarcloud.properties`. Every")
1325 A(
"number below therefore describes code this repository actually authors.")
1332 A(
"Size {#ch_size}")
1338 [
"Measure",
"Value"],
1339 [[
"Files",
"{:,}".format(int(
s.get(
"nFiles", 0)))],
1340 [
"Lines of code",
"{:,}".format(int(
s.get(
"code", 0)))],
1341 [
"Comment lines",
"{:,}".format(int(
s.get(
"comment", 0)))],
1342 [
"Blank lines",
"{:,}".format(int(
s.get(
"blank", 0)))],
1343 [
"Comment ratio",
"{:.1f} %".format(
1348 [
"Language",
"Files",
"Code",
"Comment"],
1349 [[l[
"language"],
"{:,}".format(
l.get(
"nFiles", 0)),
1350 "{:,}".format(
l.get(
"code", 0)),
"{:,}".format(
l.get(
"comment", 0))]
1351 for l
in cloc[
"languages"]],
1352 [
"---",
"--:",
"--:",
"--:"]))
1354 A(
"_cloc not available._")
1359 A(
"Cyclomatic complexity {#ch_ccn}")
1360 A(
"---------------------")
1363 A(
"Per-function CCN, measured by `lizard`. The mean is the least interesting number")
1364 A(
"here: maintenance cost lives in the tail, so the counts above each threshold are")
1365 A(
"what to compare.")
1368 [
"Measure",
"Value"],
1369 [[
"Functions",
"{:,}".format(ccn[
"functions"])],
1370 [
"Total CCN",
"{:,}".format(ccn[
"ccn_total"])],
1371 [
"Mean CCN", ccn[
"ccn_mean"]],
1372 [
"Median CCN", ccn[
"ccn_median"]],
1373 [
"90th percentile", ccn[
"ccn_p90"]],
1374 [
"99th percentile", ccn[
"ccn_p99"]],
1375 [
"Maximum CCN", ccn[
"ccn_max"]],
1376 [
"Mean function length (NLOC)", ccn[
"nloc_mean"]]],
1379 total = float(
max(1, ccn[
"functions"]))
1381 [
"Threshold",
"Functions",
"Share"],
1382 [[
"CCN > 15 (worth refactoring)",
"{:,}".format(ccn[
"over_15"]),
1383 "{:.1f} %".format(100 * ccn[
"over_15"] / total)],
1384 [
"CCN > 25 (hard to test)",
"{:,}".format(ccn[
"over_25"]),
1385 "{:.1f} %".format(100 * ccn[
"over_25"] / total)],
1386 [
"CCN > 50",
"{:,}".format(ccn[
"over_50"]),
1387 "{:.1f} %".format(100 * ccn[
"over_50"] / total)],
1388 [
"CCN > 100",
"{:,}".format(ccn[
"over_100"]),
1389 "{:.1f} %".format(100 * ccn[
"over_100"] / total)],
1390 [
"Longer than 100 lines",
"{:,}".format(ccn[
"long_over_100_lines"]),
1391 "{:.1f} %".format(100 * ccn[
"long_over_100_lines"] / total)],
1392 [
"Longer than 300 lines",
"{:,}".format(ccn[
"long_over_300_lines"]),
1393 "{:.1f} %".format(100 * ccn[
"long_over_300_lines"] / total)],
1394 [
"More than 7 parameters",
"{:,}".format(ccn[
"params_over_7"]),
1395 "{:.1f} %".format(100 * ccn[
"params_over_7"] / total)]],
1396 [
"---",
"--:",
"--:"]))
1398 A(
"### Most complex functions {#ch_ccn_worst}")
1401 [
"CCN",
"NLOC",
"Params",
"Function",
"File"],
1402 [[f[
"ccn"], f[
"nloc"], f[
"params"],
"`%s`" % f[
"name"], f[
"file"]]
1403 for f
in ccn[
"worst"]],
1404 [
"--:",
"--:",
"--:",
"---",
"---"]))
1406 A(
"### Longest functions {#ch_ccn_longest}")
1409 [
"NLOC",
"CCN",
"Function",
"File"],
1410 [[f[
"nloc"], f[
"ccn"],
"`%s`" % f[
"name"], f[
"file"]]
1411 for f
in ccn[
"longest"]],
1412 [
"--:",
"--:",
"---",
"---"]))
1414 A(
"_lizard not available._")
1420 A(
"Layering {#ch_layering}")
1424 A(
"Everything in this section is derived from the include graph. Nothing is")
1425 A(
"declared by hand, and there is no table of \"which layer should sit above")
1426 A(
"which\" - an earlier version of this panel had one, and it was wrong twice: it")
1427 A(
"encoded one reading of the architecture, and it assumed `src/` subdirectories")
1428 A(
"are modules. In darktable they are not. They are drawers - groupings of")
1429 A(
"convenience with no ownership or interface boundary - so \"common sits below")
1430 A(
"control\" is an assertion the code never made.")
1432 A(
"The primary unit here is therefore the **file**, which presumes nothing about")
1433 A(
"how the tree is organised. Directory-level figures follow, labelled as the")
1434 A(
"aggregation over drawers that they are.")
1436 A(
"Two questions, kept apart:")
1438 A(
"- **Cycles are absolute.** If A depends on B and B on A, no layering of the two")
1439 A(
" exists, whatever anyone believes. Counted as strongly connected components.")
1440 A(
"- **Backward dependencies are graduated.** Order the units so as few includes as")
1441 A(
" possible point backwards; what still does is the minimum set that would have")
1442 A(
" to go for a layering to exist at all. Computed with the Eades-Lin-Smyth")
1443 A(
" feedback-arc-set heuristic, weighted by include count.")
1446 [
"Measure",
"Files",
"Directories"],
1447 [[
"Units",
"{:,}".format((
lay.get(
"by_file")
or {}).
get(
"nodes", 0)),
1448 "{:,}".format((
lay.get(
"by_directory")
or {}).
get(
"nodes", 0))],
1449 [
"Dependency edges",
"{:,}".format((
lay.get(
"by_file")
or {}).
get(
"edges", 0)),
1450 "{:,}".format((
lay.get(
"by_directory")
or {}).
get(
"edges", 0))],
1451 [
"Cycles",
"{:,}".format(lay[
"file_cycle_count"]),
1452 "{:,}".format(lay[
"module_cycle_count"])],
1453 [
"Units caught in a cycle",
"{:,}".format(lay[
"files_in_cycles"]),
1454 "{:,}".format(lay[
"modules_in_cycles"])],
1455 [
"Backward dependencies",
"{:,}".format((
lay.get(
"by_file")
or {}).
get(
"back_edges", 0)),
1456 "{:,}".format((
lay.get(
"by_directory")
or {}).
get(
"back_edges", 0))],
1457 [
"Includes on them",
"{:,}".format((
lay.get(
"by_file")
or {}).
get(
"back_includes", 0)),
1458 "{:,}".format((
lay.get(
"by_directory")
or {}).
get(
"back_includes", 0))],
1459 [
"Share of all includes",
"{} %".format((
lay.get(
"by_file")
or {}).
get(
"back_ratio", 0)),
1460 "{} %".format((
lay.get(
"by_directory")
or {}).
get(
"back_ratio", 0))]],
1461 [
"---",
"--:",
"--:"]))
1464 if bf
and bf[
"worst"]:
1465 A(
"### Backward dependencies between files {#ch_layering_files}")
1467 A(
"Each of these is an include that could not be ordered away. Removing this set")
1468 A(
"would leave a tree that can be laid out in strict layers.")
1471 [
"Include",
"From rank",
"To rank"],
1472 [[
"`%s`" % v[
"pair"], v[
"from_rank"], v[
"to_rank"]]
for v
in bf[
"worst"]],
1473 [
"---",
"--:",
"--:"]))
1476 if bd
and bd[
"worst"]:
1477 A(
"### Backward dependencies between directories {#ch_layering_dirs}")
1479 A(
"Aggregated over `src/` subdirectories. Read with the caveat above: these are")
1480 A(
"drawers, so a large number here says the include flow between two drawers is")
1481 A(
"two-way, not that a designed boundary was broken.")
1483 A(
"Derived order, rank 0 first:")
1485 A(
"> " +
" < ".
join(
"`%s`" % m[
"name"]
for m
in bd[
"order"]))
1488 [
"Includes",
"Points backwards",
"From rank",
"To rank"],
1489 [[v[
"includes"],
"`%s`" % v[
"pair"], v[
"from_rank"], v[
"to_rank"]]
1490 for v
in bd[
"worst"]],
1491 [
"--:",
"---",
"--:",
"--:"]))
1493 if lay[
"module_cycles"]:
1494 A(
"### Directory dependency cycles {#ch_layering_cycles}")
1497 [[len(c),
", ".
join(
"`%s`" % m
for m
in c)]
1498 for c
in lay[
"module_cycles"]],
1502 A(
"### Cyclic header clusters {#ch_layering_filecycle}")
1504 A(
"Each block below is a strongly connected component: a set of files that all")
1505 A(
"reach one another through includes. **A component is not one cycle.** A")
1506 A(
"seven-header component contains many distinct cycles, which is why the count")
1507 A(
"of components is a poor headline and the size, and the number of includes that")
1508 A(
"must be cut to break it, are given instead.")
1511 [
"Files",
"Internal includes",
"Cuts to break"],
1512 [[c[
"size"], c[
"internal_edges"], c[
"cuts_to_break"]]
1513 for c
in lay[
"file_cycles"]],
1514 [
"--:",
"--:",
"--:"]))
1516 for i, c
in enumerate(lay[
"file_cycles"], 1):
1517 A(
"**Cluster %d** - %d files, %d cuts to break:"
1518 % (i, c[
"size"], c[
"cuts_to_break"]))
1520 for f
in c[
"files"]:
1524 A(
"Cutting these breaks it:")
1526 for e
in c[
"cut_edges"]:
1531 A(
"### Stability {#ch_layering_stability}")
1533 A(
"A second graph-derived view, independent of the ordering above; two")
1534 A(
"derivations agreeing is worth more than either alone. `Ca` counts the")
1535 A(
"directories depending on one, `Ce` those it depends on, and instability is")
1536 A(
"`I = Ce / (Ca + Ce)`. `I = 0` means everything depends on it and it depends")
1537 A(
"on nothing - expensive to change. `I = 1` means nothing depends on it.")
1539 A(
"The Stable Dependencies Principle says a unit should depend only on units at")
1540 A(
"least as stable as itself; an edge with `I(from) < I(to)` breaks it, letting a")
1541 A(
"volatile unit's churn propagate into a stable one. Computed over directories,")
1542 A(
"so it inherits their weakness as a unit.")
1545 [
"Measure",
"Value"],
1546 [[
"Edges breaking the principle",
"{:,}".format(st[
"violating_edges"])],
1547 [
"Includes on those edges",
"{:,}".format(st[
"violating_includes"])],
1548 [
"Share of cross-directory includes",
"{} %".format(st[
"violation_ratio"])]],
1552 [
"Directory",
"Ca",
"Ce",
"I"],
1553 [[
"`%s`" % r[
"module"], r[
"Ca"], r[
"Ce"], r[
"I"]]
for r
in st[
"modules"]],
1554 [
"---",
"--:",
"--:",
"--:"]))
1557 A(
"_include data not available (needs Doxygen's SQLite output)._")
1560 A(
"### The application-global header {#ch_layering_god}")
1562 A(
"`%s` is the header every fork of this codebase inherits. A `.c` including it"
1564 A(
"is a choice local to that file; a **header** including it pushes the whole")
1565 A(
"application into every file downstream, which is how an include graph stops")
1566 A(
"being a graph and becomes a mesh.")
1569 [
"Included by",
"Count"],
1570 [[
"Headers",
"{:,}".format(god[
"included_by_headers"])],
1571 [
"Source files",
"{:,}".format(god[
"included_by_sources"])],
1572 [
"Total",
"{:,}".format(god[
"total"])]],
1576 A(
"Headers that include it:")
1578 for h
in god[
"headers"]:
1585 A(
"Transitive reach {#ch_reach}")
1586 A(
"----------------")
1588 A(
"Direct fan-in undercounts, and cycle counts miss this entirely. A header that")
1589 A(
"only 40 files include, but which those 40 pass on, can still end up under most")
1590 A(
"of the codebase. What follows is the transitive answer: how much of the tree")
1591 A(
"depends on each header, and how much each translation unit drags in.")
1593 A(
"### Propagation cost {#ch_reach_prop}")
1595 A(
"Propagation cost is the mean share of the system a file can reach: the")
1596 A(
"probability that a change to a random file can, in principle, reach a random")
1597 A(
"other one. It is the standard summary of architectural coupling")
1598 A(
"(MacCormack, Baldwin & Rusnak), and the single number this section is circling.")
1600 A(
"The **core** is the largest group of files that all reach one another. Being a")
1601 A(
"cyclic group it has no internal layering by definition, so it can only be")
1602 A(
"understood as a unit. Everything else is classified against the core's")
1603 A(
"thresholds. A healthy system has a small core and a large periphery.")
1606 [
"Measure",
"Value"],
1607 [[
"Propagation cost",
"{} %".format(rch[
"propagation_cost"])],
1608 [
"Core size",
"{:,} files ({} %)".format(rch[
"core_size"], rch[
"core_share"])]]
1610 for k, v
in sorted(
rch.get(
"buckets", {}).
items(), key=
lambda kv: -kv[1])],
1614 A(
"Files in the core:")
1616 for f
in rch[
"core_files"]:
1620 [
"Measure",
"Value"],
1621 [[
"Files in the graph",
"{:,}".format(rch[
"files"])],
1622 [
"Headers reaching over half the tree",
"{:,}".format(rch[
"headers_over_half"])],
1623 [
"Headers reaching over a quarter",
"{:,}".format(rch[
"headers_over_quarter"])],
1624 [
"Headers pulled in per source file, median",
"{:,}".format(rch[
"median_headers_pulled"])],
1625 [
"Headers pulled in per source file, mean", rch[
"mean_headers_pulled"]],
1626 [
"Worst",
"{:,}".format(rch[
"max_headers_pulled"])]],
1629 A(
"### Headers most of the codebase depends on {#ch_reach_dependents}")
1631 A(
"Changing one of these means rebuilding, and re-reviewing, that share of the")
1632 A(
"tree. This is the cost a god header imposes, and it is invisible to every")
1633 A(
"cycle metric: a header can sit in no cycle at all and still be here.")
1636 [
"Dependents",
"Share of tree",
"Drags in",
"Header"],
1637 [[
"{:,}".format(r[
"dependents"]),
"{} %".format(r[
"share"]),
1638 "{:,}".format(r[
"drags_in"]),
"`%s`" % r[
"file"]]
1639 for r
in rch[
"top_dependents"]],
1640 [
"--:",
"--:",
"--:",
"---"]))
1642 A(
"### Heaviest supply lines {#ch_reach_burden}")
1644 A(
"A header should include only what its own declarations need. Anything beyond")
1645 A(
"that is a supply line its consumers never asked for and cannot see: they compile")
1646 A(
"because something upstream happened to pull in what they use, and the day anyone")
1647 A(
"tidies that away the breakage surfaces in a file nobody touched.")
1649 A(
"`Drags in` is how many headers arrive with this one. Multiplied by its")
1650 A(
"dependents, it gives the file-header pairs this single header is responsible for")
1651 A(
"across the tree - the weight it actually imposes, rather than how popular it is.")
1654 [
"Burden",
"Dependents",
"Drags in",
"Header"],
1655 [[
"{:,}".format(r[
"burden"]),
"{:,}".format(r[
"dependents"]),
1656 "{:,}".format(r[
"drags_in"]),
"`%s`" % r[
"file"]]
1657 for r
in rch[
"top_burden"]],
1658 [
"--:",
"--:",
"--:",
"---"]))
1660 A(
"### Translation units pulling in the most headers {#ch_reach_depth}")
1663 [
"Headers pulled",
"File"],
1664 [[
"{:,}".format(r[
"headers_pulled"]),
"`%s`" % r[
"file"]]
for r
in rch[
"deepest"]],
1670 A(
"Header coupling {#ch_coupling}")
1671 A(
"---------------")
1674 A(
"Direct fan-in: how many files include each header. This is the number behind the")
1675 A(
'"included by" graphs on each file page, and the clearest single measure of how')
1676 A(
"entangled the headers are. A header near the top of this table cannot be changed")
1677 A(
"without rebuilding, and re-reviewing, most of the codebase.")
1680 [
"Included by",
"Header"],
1681 [[r[
"included_by"], r[
"file"]]
for r
in inc[:40]],
1684 A(
"_include data not available (needs Doxygen's SQLite output)._")
1689 A(
"Symbols per file {#ch_symbols}")
1690 A(
"----------------")
1693 tot =
sum(r[
"total"]
for r
in sym)
1694 A(
"From Doxygen's own symbol table. A file with a very large symbol count is doing")
1695 A(
"more than one job; a header with one is an interface.")
1698 [
"Measure",
"Value"],
1699 [[
"Files with symbols",
"{:,}".format(len(sym))],
1700 [
"Symbols total",
"{:,}".format(tot)],
1701 [
"Mean per file",
"{:.1f}".format(tot / float(
max(1, len(sym))))],
1702 [
"Files with > 100 symbols",
"{:,}".format(
sum(1
for r
in sym
if r[
"total"] > 100))],
1703 [
"Files with > 50 symbols",
"{:,}".format(
sum(1
for r
in sym
if r[
"total"] > 50))]],
1706 A(
"### Largest interfaces {#ch_symbols_top}")
1709 [
"Symbols",
"Functions",
"Variables",
"Typedefs",
"Macros",
"Enums",
"File"],
1710 [[r[
"total"], r[
"functions"], r[
"variables"], r[
"typedefs"],
1711 r[
"macros"], r[
"enums"], r[
"file"]]
for r
in sym[:60]],
1712 [
"--:",
"--:",
"--:",
"--:",
"--:",
"--:",
"---"]))
1714 A(
"The complete per-file table is in `code-health.json`, published next to this page.")
1716 A(
"_symbol data not available (needs Doxygen's SQLite output)._")
1721 A(
"Modularity {#ch_modularity}")
1725 A(
"Do the folders correspond to how the code is actually coupled?")
1727 A(
"The directory layout is treated as a proposed grouping of the dependency graph")
1728 A(
"and scored with Newman modularity `Q` - the share of edges falling inside")
1729 A(
"groups, minus what random wiring of the same degrees would give. Then a grouping")
1730 A(
"is derived from the graph itself, with Louvain, and scored the same way.")
1732 A(
"**The gap is the answer.** If directories really were modules, grouping by")
1733 A(
"directory would be near-optimal and the gap would be small. A large gap means")
1734 A(
"the code does cluster - just not along the lines it is filed under.")
1737 [
"Measure",
"Value"],
1738 [[
"Q of the directory layout", mod[
"q_directories"]],
1739 [
"Q of the derived grouping", mod[
"q_derived"]],
1740 [
"Gap", mod[
"gap"]],
1741 [
"Directories",
"{:,}".format(mod[
"directories"])],
1742 [
"Derived clusters",
"{:,}".format(mod[
"derived_clusters"])],
1743 [
"Files whose directory matches their cluster",
1744 "{:,} ({} %)".format(mod[
"files_in_agreeing_cluster"], mod[
"agreement_share"])],
1745 [
"Includes staying inside one directory",
1746 "{:,} of {:,} ({} %)".format(mod[
"intra_directory_includes"],
1747 mod[
"total_includes"],
1748 mod[
"intra_directory_share"])]],
1751 A(
"Largest derived clusters, in files: " +
1752 ", ".
join(
str(n)
for n
in mod[
"largest_clusters"]))
1754 A(
"### How to read this, and how not to {#ch_modularity_caveat}")
1756 A(
"Modularity rewards COMMUNITY structure - groups with dense internal and sparse")
1757 A(
"external links. A well-layered codebase is not community-structured, it is")
1758 A(
"hierarchical, and the two are different shapes. A leaf library factored out")
1759 A(
"precisely so that everything can use it has, by construction, almost all its")
1760 A(
"edges crossing a boundary, and `Q` marks it down for exactly the property that")
1761 A(
"makes it good design.")
1763 A(
"So a lower `Q` is not automatically worse, and this metric should not be read")
1764 A(
"as a verdict the way the cycle and reach figures can be. What it does say")
1765 A(
"reliably is the GAP: both scores here are far below the 0.3 that usually")
1766 A(
"indicates real community structure, while the derived grouping clears it. The")
1767 A(
"code clusters; the folders are not where it clusters. That holds whichever tree")
1768 A(
"is measured, and it is the honest form of the observation that `src/`")
1769 A(
"subdirectories are drawers rather than modules.")
1772 A(
"_include data not available._")
1776 A(
"Header self-containment {#ch_selfcontained}")
1777 A(
"-----------------------")
1780 A(
"Every header compiled on its own, as a translation unit containing nothing but")
1781 A(
"an include of itself. A header that fails is relying on whoever includes it")
1782 A(
"having pulled something in first - the dependency is real and written nowhere,")
1783 A(
"and it breaks the day someone tidies an include in a file that never mentioned")
1786 A(
"X-macro headers are excluded: they are re-included several times in one")
1787 A(
"translation unit with different macros defined, so compiling one alone is not a")
1788 A(
"question that applies.")
1791 [
"Measure",
"Value"],
1792 [[
"Headers checked",
"{:,}".format(sc[
"headers"])],
1793 [
"Self-contained",
"{:,}".format(sc[
"self_contained"])],
1794 [
"Share",
"{} %".format(sc[
"share"])],
1795 [
"Failing",
"{:,}".format(sc[
"failing_count"])]],
1800 [[
"`%s`" % h, e]
for h, e
in sc[
"failing"]],
1804 A(
"_no self-containment report was supplied. It needs a configured build tree, so")
1805 A(
"the code-health workflow produces it and this build folds it in._")
1809 A(
"Documentation coverage {#ch_docs}")
1810 A(
"----------------------")
1813 A(
"Symbols carrying a brief or detailed description, from Doxygen's own record.")
1814 A(
"A low figure is not automatically bad - self-explanatory code needs no prose -")
1815 A(
"but it bounds how much of the API can be understood without reading its")
1816 A(
"implementation.")
1819 [
"Measure",
"Value"],
1820 [[
"Symbols",
"{:,}".format(doc[
"symbols"])],
1821 [
"Documented",
"{:,}".format(doc[
"documented"])],
1822 [
"Coverage",
"{} %".format(doc[
"share"])]],
1826 [
"Kind",
"Symbols",
"Documented",
"Coverage"],
1827 [[k[
"kind"],
"{:,}".format(k[
"symbols"]),
"{:,}".format(k[
"documented"]),
1828 "{} %".format(k[
"share"])]
for k
in doc[
"by_kind"]],
1829 [
"---",
"--:",
"--:",
"--:"]))
1831 A(
"Files with the most undocumented symbols:")
1834 [
"Undocumented",
"of",
"File"],
1835 [[
"{:,}".format(w[
"undocumented"]),
"{:,}".format(w[
"symbols"]),
"`%s`" % w[
"file"]]
1836 for w
in doc[
"worst_files"]],
1837 [
"--:",
"--:",
"---"]))
1839 A(
"_symbol data not available (needs Doxygen's SQLite output)._")
1843 A(
"Change history {#ch_git}")
1847 A(
"Process metrics, over the last %d days. These predict defects better than" % g[
"days"])
1848 A(
"static complexity does, and nothing else in this panel can see them: they are")
1849 A(
"not a property of the code as it stands but of how it got there. Complex code")
1850 A(
"nobody touches is harmless; complex code changed weekly is where bugs live.")
1853 [
"Measure",
"Value"],
1854 [[
"Commits",
"{:,}".format(g[
"commits"])],
1855 [
"Files touched",
"{:,}".format(g[
"files_touched"])],
1856 [
"Lines added + deleted",
"{:,}".format(g[
"total_churn"])],
1857 [
"Authors per file, mean", g[
"mean_authors"]],
1858 [
"Authors per file, most",
"{:,}".format(g[
"max_authors"])],
1859 [
"Files with a single author",
"{:,}".format(g[
"single_author_files"])]],
1862 A(
"### Hotspots {#ch_git_hotspots}")
1864 A(
"Revisions multiplied by cyclomatic complexity. This is the prioritisation")
1865 A(
"metric: what to refactor first, rather than what is merely large. A file high")
1866 A(
"on this list is both hard to reason about and constantly being reasoned about.")
1869 [
"Score",
"Revisions",
"CCN",
"Churn",
"File"],
1870 [[
"{:,}".format(h[
"score"]), h[
"revisions"],
"{:,}".format(h[
"ccn"]),
1871 "{:,}".format(h[
"churn"]),
"`%s`" % h[
"file"]]
for h
in g[
"hotspots"]],
1872 [
"--:",
"--:",
"--:",
"--:",
"---"]))
1874 A(
"### Change coupling {#ch_git_coupling}")
1876 A(
"Files that keep being changed together. Some of these pairs have no include")
1877 A(
"edge between them at all, which is coupling no static analysis can find -")
1878 A(
"a shared assumption, a duplicated constant, two halves of one idea kept in")
1879 A(
"step by hand. `Confidence` is how often the rarer of the two changes brings")
1880 A(
"the other with it.")
1882 A(
"Commits touching more than 20 files are left out of these counts: a sweeping")
1883 A(
"rename couples everything it touches to everything else, which says nothing")
1884 A(
"about the code.")
1886 A(
"%d pairs meet the threshold (5+ shared commits, 40%%+ confidence)."
1887 % g[
"coupled_total"])
1890 [
"Together",
"Confidence",
"Pair"],
1891 [[c[
"together"],
"{:.0f} %".format(c[
"confidence"]),
"`%s`" % c[
"pair"]]
1892 for c
in g[
"coupled"]],
1893 [
"--:",
"--:",
"---"]))
1895 A(
"### Most revised files {#ch_git_revised}")
1898 [
"Revisions",
"Churn",
"File"],
1899 [[m[
"revisions"],
"{:,}".format(m[
"churn"]),
"`%s`" % m[
"file"]]
1900 for m
in g[
"most_revised"]],
1901 [
"--:",
"--:",
"---"]))
1903 A(
"_no git history available. The documentation job must check out with")
1904 A(
"`fetch-depth: 0`; a shallow clone has nothing to measure._")
1907 A(
"Static analysis {#ch_static}")
1908 A(
"---------------")
1911 A(
"### cppcheck {#ch_cppcheck}")
1915 [[k,
"{:,}".format(v)]
for k, v
in cpp[
"by_severity"].
items()],
1919 [[
"{:,}".format(n),
"`%s`" % r]
for r, n
in cpp[
"top_rules"]],
1922 A(
"_cppcheck not available._")
1925 A(
"### clang-tidy {#ch_clang_tidy}")
1929 [[
"Findings",
"{:,}".format(ct[
"total"])],
1930 [
"Files with findings",
"{:,}".format(ct[
"files_with_findings"])]],
1934 [[
"{:,}".format(n),
"`%s`" % c]
for c, n
in ct[
"top_checks"]],
1937 A(
"_No clang-tidy report was supplied to this build. clang-tidy needs a configured")
1938 A(
"build tree (`compile_commands.json`), which the documentation job does not")
1939 A(
"produce; the separate code-health workflow supplies it when it has run._")
1941 A(
"Elsewhere {#ch_elsewhere}")
1944 A(
"SonarCloud carries the findings this panel does not: rule-level issues, duplication,")
1945 A(
"cognitive complexity and technical debt, with the same third-party exclusions.")
1947 A(
"- darktable 5.0: <https://sonarcloud.io/project/overview?id=aurelienpierreeng_darktable-5>")
1948 A(
"- Ansel: <https://sonarcloud.io/project/overview?id=aurelienpierreeng_ansel>")
1950 return "\n".
join(L) +
"\n"
1959 help=
"where to read .gitmodules from (default: cwd)")
1963 help=
"JSON from tools/check_header_selfcontained.py")
1968 help=
"window for the git evolution metrics (default: one year)")
1974 % (len(added),
", ".
join(added)
or "none"))
1976 data = {
"project":
args.project,
"excluded_submodules": added}
1983 except Exception
as exc:
1993 src_edges = step(
"includes (source)",
1995 merged = sorted(set(dox_edges
or []) | set(src_edges
or []))
1996 sys.stderr.write(
"code_health: include graph %d doxygen + %d source -> %d union\n"
1997 % (len(dox_edges
or []), len(src_edges
or []), len(merged)))
1998 edges = merged
or None
1999 data[
"include_graph"] = {
2000 "doxygen_edges": len(dox_edges
or []),
2001 "source_edges": len(src_edges
or []),
2002 "union_edges": len(merged),
2006 data[
"reach"] = step(
"transitive reach",
lambda:
collect_reach(edges))
2007 data[
"modularity"] = step(
"modularity",
2009 data[
"selfcontained"] = step(
"header self-containment",
2012 data[
"git"] = step(
"git history",
2017 data[
"cppcheck"] = step(
"cppcheck",
2026 with open(path,
"w", encoding=
"utf-8")
as fh:
2032if __name__ ==
"__main__":
static const float const float const float min
const dt_collection_sort_t items[]
static float * partition(float *first, float *last, float val)
derive_layering(edges, label)
collect_layering(edges, source_dir="src")
compute_stability(mod_edges)
build_markdown(project, data)
load_submodule_exclusions(repo_root=".")
strongly_connected(nodes, succ)
collect_god_header(edges)
module_of(path, source_dir="src")
collect_cppcheck(source_dir, jobs)
collect_git(repo_root, days, per_file_ccn, max_files_per_commit=20)
_modularity(adj_w, degree, two_m, partition)
collect_selfcontained(path)
collect_clang_tidy(log_path)
source_include_edges(repo_root, source_dir="src")
feedback_arc_order(edges)
md_table(headers, rows, aligns=None)
collect_modularity(edges, source_dir="src")
_louvain(adj_w, degree, two_m, passes=12)