docs(doxygen): fix critical extraction bug; baseline 24% → 42% on public API
ROOT CAUSE FIX
The Doxyfile EXCLUDE_PATTERNS line contained `*/* 2.hpp` (note the
space — a stray glob from macOS-style "foo 2.hpp" duplicate files).
That pattern was silently matching ALL .hpp / .h files, so Doxygen was
indexing nothing under code/include/. The pre-existing 556 KB of HTML
output was effectively documenting only README.md, CLAUDE.md and a
small stub for std:: — not the C++ API at all.
After fixing the pattern (and properly escaping the space-prefixed
"foo 2.hpp / foo 2.h" macOS-dup patterns), Doxygen now extracts 141
compounds and emits 248 HTML pages from the public headers.
WHAT THIS PR ADDS
1. Doxyfile fix: correct EXCLUDE_PATTERNS; add GENERATE_XML for the
coverage measurement script; add MathJax for `$$...$$` math in
markdown; add the missing CGAL `\cgalParamNBegin/End/Description/
Default/...` aliases so CGAL-style param blocks render correctly.
2. New headers:
- code/include/CGAL/Conformal_map/doxygen_groups.h
defines `PkgConformalMap{,Ref,Concepts,NamedParameters}`,
resolving 17 prior "non-existing group" warnings.
- code/include/CGAL/Conformal_map/doxygen_namespaces.h
gives every namespace under `CGAL::` and `conformallab::` a
brief description.
3. New tool: scripts/doxygen-coverage.sh
Parses the XML output and reports % of public symbols (excluding
the `detail::` implementation namespaces by default) that have a
non-empty brief/detailed description. Supports `--list-undoc`
and `--threshold N` for CI integration.
4. Substantial docstring additions to the public CGAL headers:
`Conformal_map_traits.h`, `Discrete_circle_packing.h`,
`Discrete_inversive_distance.h`, `conformal_mesh.hpp`,
`Discrete_conformal_map.h` (Hyper_ideal_map_result fields).
5. Markdown housekeeping that the strict-warning Doxygen run surfaced:
tests.md (escape literal `#` in table cell),
locked-vs-flexible.md (broken section anchor),
overall_pipeline.md (replace `$$LaTeX$$` with inline-unicode math).
CURRENT NUMBERS
before: ~24% documented (public API; the prior "87%" claim was
based on the broken extraction)
after: 42% documented (165 of 396 public symbols)
warnings: 0 (was 27 spurious + a flood of bogus undocumented
warnings hidden by the buggy EXCLUDE pattern)
NEXT (in a follow-up commit on this branch)
The remaining 231 public symbols (mostly in `layout.hpp`,
`hyper_ideal_functional.hpp`, `spherical_functional.hpp`, the per-mode
functional/Hessian files) can be brought to ~100% with another pass of
short `///` brief descriptions. The coverage script is the gate; CI
can begin enforcing `--threshold 95` once the next pass lands.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
159
scripts/doxygen-coverage.sh
Executable file
159
scripts/doxygen-coverage.sh
Executable file
@@ -0,0 +1,159 @@
|
||||
#!/bin/bash
|
||||
# scripts/doxygen-coverage.sh
|
||||
#
|
||||
# Measure Doxygen documentation coverage of the public C++ API by parsing
|
||||
# the Doxygen XML output. Reports:
|
||||
# * total documentable members (functions, classes, structs, enums,
|
||||
# typedefs, variables) in code/include/**
|
||||
# * how many have a non-empty briefdescription/detaileddescription
|
||||
# * coverage % and list of undocumented members
|
||||
#
|
||||
# Prerequisite: doxygen must have been run with GENERATE_XML=YES (which
|
||||
# the project's Doxyfile sets). This script invokes it if XML is missing.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/doxygen-coverage.sh # short summary
|
||||
# bash scripts/doxygen-coverage.sh --list-undoc # list undocumented members
|
||||
# bash scripts/doxygen-coverage.sh --threshold 95 # fail if coverage < 95 %
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 coverage ≥ threshold (default 0 — informational only)
|
||||
# 1 coverage < threshold
|
||||
# 2 XML output missing / could not be parsed
|
||||
|
||||
set -eu
|
||||
|
||||
XML_DIR="doc/doxygen/xml"
|
||||
THRESHOLD=0
|
||||
LIST_UNDOC=0
|
||||
|
||||
INCLUDE_DETAIL=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--threshold) THRESHOLD="$2"; shift 2 ;;
|
||||
--list-undoc) LIST_UNDOC=1; shift ;;
|
||||
--include-detail) INCLUDE_DETAIL=1; shift ;;
|
||||
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ! -d "$XML_DIR" ]; then
|
||||
echo "XML output missing — running doxygen..."
|
||||
doxygen Doxyfile >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [ ! -d "$XML_DIR" ]; then
|
||||
echo "ERROR: $XML_DIR still missing after doxygen run" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
python3 - "$XML_DIR" "$LIST_UNDOC" "$THRESHOLD" "$INCLUDE_DETAIL" <<'PYEOF'
|
||||
import sys, os, glob, xml.etree.ElementTree as ET
|
||||
|
||||
xml_dir, list_undoc, threshold, include_detail = \
|
||||
sys.argv[1], int(sys.argv[2]), float(sys.argv[3]), int(sys.argv[4])
|
||||
|
||||
# Implementation-detail namespaces — not part of the public API surface.
|
||||
# Skipped by default; pass --include-detail to count them too.
|
||||
DETAIL_NAMES = ("::detail::", "::detail_xml::", "::cp_detail::", "::id_detail::",
|
||||
"::detail$", "::detail_xml$", "::cp_detail$", "::id_detail$")
|
||||
|
||||
def is_detail(qualified_name: str) -> bool:
|
||||
if include_detail:
|
||||
return False
|
||||
return any(qualified_name.find(d.rstrip("$")) >= 0 for d in DETAIL_NAMES)
|
||||
|
||||
# Restrict to compounds whose location is under code/include/ (the
|
||||
# public API). XML output also includes README.md and CLAUDE.md as
|
||||
# "file" kind compounds, which we want to skip.
|
||||
PUBLIC_PREFIX = os.path.abspath("code/include") + os.sep
|
||||
|
||||
KINDS = {"function", "class", "struct", "enum", "typedef", "variable", "namespace"}
|
||||
|
||||
total = 0
|
||||
documented = 0
|
||||
undoc = []
|
||||
|
||||
for path in sorted(glob.glob(os.path.join(xml_dir, "*.xml"))):
|
||||
if os.path.basename(path) in {"index.xml", "Doxyfile.xml", "indexpage.xml"}:
|
||||
continue
|
||||
if os.path.basename(path).startswith(("namespacestd", "md_")):
|
||||
continue
|
||||
try:
|
||||
tree = ET.parse(path)
|
||||
except ET.ParseError:
|
||||
continue
|
||||
for cd in tree.iter("compounddef"):
|
||||
kind = cd.attrib.get("kind", "")
|
||||
# only count compounds living in our public include tree
|
||||
loc = cd.find("location")
|
||||
if loc is None:
|
||||
continue
|
||||
file_attr = loc.attrib.get("file", "")
|
||||
if not file_attr.startswith(PUBLIC_PREFIX) and \
|
||||
not file_attr.startswith("code/include/"):
|
||||
continue
|
||||
|
||||
# The compound itself
|
||||
if kind in {"class", "struct", "namespace"}:
|
||||
cname = cd.findtext("compoundname", "?")
|
||||
if not is_detail(cname):
|
||||
total += 1
|
||||
brief = cd.find("briefdescription")
|
||||
detail = cd.find("detaileddescription")
|
||||
has_doc = (brief is not None and len("".join(brief.itertext()).strip()) > 0) or \
|
||||
(detail is not None and len("".join(detail.itertext()).strip()) > 0)
|
||||
if has_doc:
|
||||
documented += 1
|
||||
else:
|
||||
undoc.append(f"{kind:9s} {cname} ({file_attr}:{loc.attrib.get('line','?')})")
|
||||
|
||||
# Members inside the compound
|
||||
for memberdef in cd.iter("memberdef"):
|
||||
mkind = memberdef.attrib.get("kind", "")
|
||||
if mkind not in KINDS:
|
||||
continue
|
||||
prot = memberdef.attrib.get("prot", "public")
|
||||
if prot != "public":
|
||||
continue
|
||||
name = memberdef.findtext("name", "?")
|
||||
qual = memberdef.findtext("qualifiedname", name)
|
||||
if is_detail(qual):
|
||||
continue
|
||||
total += 1
|
||||
brief = memberdef.find("briefdescription")
|
||||
detail = memberdef.find("detaileddescription")
|
||||
has_doc = (brief is not None and len("".join(brief.itertext()).strip()) > 0) or \
|
||||
(detail is not None and len("".join(detail.itertext()).strip()) > 0)
|
||||
if has_doc:
|
||||
documented += 1
|
||||
else:
|
||||
mloc = memberdef.find("location")
|
||||
fl = mloc.attrib.get("file", "?") if mloc is not None else "?"
|
||||
ln = mloc.attrib.get("line", "?") if mloc is not None else "?"
|
||||
undoc.append(f"{mkind:9s} {qual} ({fl}:{ln})")
|
||||
|
||||
if total == 0:
|
||||
print("ERROR: no public members found — check that GENERATE_XML=YES and EXTRACT_ALL=YES")
|
||||
sys.exit(2)
|
||||
|
||||
pct = 100.0 * documented / total
|
||||
print(f"Doxygen coverage (public symbols under code/include/):")
|
||||
print(f" documented: {documented}")
|
||||
print(f" total: {total}")
|
||||
print(f" coverage: {pct:.1f}%")
|
||||
print(f" undocumented: {total - documented}")
|
||||
|
||||
if list_undoc:
|
||||
print()
|
||||
print("Undocumented symbols:")
|
||||
for s in undoc:
|
||||
print(f" {s}")
|
||||
|
||||
if pct < threshold:
|
||||
print(f"\nFAIL: coverage {pct:.1f}% < threshold {threshold}%", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0)
|
||||
PYEOF
|
||||
Reference in New Issue
Block a user