From e25457d5ff9f3e7635664cd6df6d549361ff2563 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 06:58:08 +0100 Subject: [PATCH 1/2] ci: S23.22 assert every platform class carries its registry token docs/NAMING.md states the rule and nothing checked it, which is how the naming drifted far enough to need the rename in #721. Kept apart from the ALIASES map: that one is prose vocabulary, so reusing it would make SolidSyslogPOSIXFile a legal declaration. Part of #708 --- .github/workflows/ci.yml | 5 +-- docs/NAMING.md | 5 +++ scripts/check_platform_docs.py | 63 +++++++++++++++++++++++++++++----- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae56cb95..2a6f8f24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -777,8 +777,9 @@ jobs: # the docs enumerate exactly what the code declares — a platform's pages, # nav entry, description and matrix row; a role's link from the porting # guide, the roles index and the nav — and the reverse in both cases. It - # also holds the two boundaries hand review kept losing: no platform names - # another, and every class a platform ships is on its page. + # also holds the three boundaries hand review kept losing: no platform + # names another, every class a platform declares carries its registry + # token, and every class a platform ships is on its page. - name: Check the docs match what the code declares run: python3 scripts/check_platform_docs.py diff --git a/docs/NAMING.md b/docs/NAMING.md index 20a52484..5e0d8a6b 100644 --- a/docs/NAMING.md +++ b/docs/NAMING.md @@ -30,6 +30,11 @@ split between the two tools so they cannot disagree on the same name: - **cppcheck-misra** is the sole authority on naming *uniqueness*. The MISRA addon surfaces rules 5.1, 5.2, 5.4, 5.6, 5.7, 5.8 and 5.9 violations, which pattern matching alone cannot detect. +- **`scripts/check_platform_docs.py`** is the sole authority on the one + rule neither tool can see — that a platform class carries its pack's + registry token, described under *Platform classes carry their pack's + registry token* below. The token is declared in CMake rather than in + the code, so no check reading only the translation unit can know it. cppcheck's `naming` addon is deliberately not used: it does less than clang-tidy on every axis we care about, and a second tool checking the diff --git a/scripts/check_platform_docs.py b/scripts/check_platform_docs.py index e14e9dfb..eeea4dc6 100644 --- a/scripts/check_platform_docs.py +++ b/scripts/check_platform_docs.py @@ -19,12 +19,16 @@ folder or a group with no row behind it. Registering a platform is then the one edit that cannot be forgotten, because forgetting anything else fails the build. -It also holds two boundaries that hand review kept losing: +It also holds three boundaries that hand review kept losing: * **No platform names another.** A platform describes itself completely; where a capability comes from is the capability matrix's job. Naming a sibling couples the two, so the eleventh platform means editing ten pages. Applies to the platform's whole tree and its docs folder alike. +* **Every class a platform declares carries its token.** The rule is stated in + docs/NAMING.md, "Platform classes carry their pack's registry token", and had + nothing asserting it — which is how the naming drifted far enough to need a + rename. * **Every header its platform ships is on its page.** The *What it ships* manifest is generated from the Interface directory, so this asserts the heading the generator writes into is still there. @@ -68,6 +72,16 @@ "Windows": ["Winsock", "Win32"], } +# A pack wrapping a genuinely distinct second upstream prefixes those classes +# with that upstream instead of its token — docs/NAMING.md, "Platform classes +# carry their pack's registry token". Kept apart from ALIASES above, which is +# prose vocabulary: "POSIX" is how the Posix platform is written in a sentence, +# and must not become a licence to declare SolidSyslogPOSIXFile. An entry is +# agreed when the second upstream is taken on, so widening this is deliberate. +CLASS_PREFIXES = { + "Windows": ["Winsock"], +} + # Deliberate exemptions, each with the reason it is not a boundary breach. # Printed on every run: an exemption nobody sees is an exemption nobody # revisits. @@ -115,6 +129,14 @@ def documented(): } +def interface_headers(directory): + """The header filenames a platform declares, sorted. Empty if it declares none.""" + interface = os.path.join(ROOT, directory, "Interface") + if not os.path.isdir(interface): + return [] + return sorted(name for name in os.listdir(interface) if name.endswith(".h")) + + def vocabulary(rows): """Every term that names a platform, mapped to the tokens that may use it. @@ -130,12 +152,10 @@ def vocabulary(rows): for token, directory in rows: for alias in [token] + ALIASES.get(token, []): terms.setdefault(alias, set()).add(token) - interface = os.path.join(ROOT, directory, "Interface") - for header in sorted(os.listdir(interface)) if os.path.isdir(interface) else []: - if header.endswith(".h"): - stem = header[: -len(".h")] - terms.setdefault(stem, set()).add(token) - terms.setdefault(stem[len("SolidSyslog") :], set()).add(token) + for header in interface_headers(directory): + stem = header[: -len(".h")] + terms.setdefault(stem, set()).add(token) + terms.setdefault(stem[len("SolidSyslog") :], set()).add(token) return terms @@ -240,6 +260,26 @@ def role_faults(): return faults +def prefix_faults(rows): + """Every header a platform declares begins with SolidSyslog. + + docs/NAMING.md states this for public classes. An *Errors.h takes its name + from the class it belongs to, so it carries the token by construction and is + checked alongside the rest — one that does not is an orphan worth catching. + """ + faults = [] + for token, directory in rows: + accepted = [token] + CLASS_PREFIXES.get(token, []) + for header in interface_headers(directory): + if not any(header.startswith(f"SolidSyslog{prefix}") for prefix in accepted): + wanted = " or ".join(f"SolidSyslog{prefix}" for prefix in accepted) + faults.append( + f"{token}: {directory}/Interface/{header} does not begin {wanted} — " + "a platform's classes carry its registry token" + ) + return faults + + def unlisted_headers(rows): """The *What it ships* manifest is generated by hooks/platform_backlinks.py from the platform's own Interface directory, so every header is listed by @@ -286,6 +326,7 @@ def check(): for slug in sorted(documented() - slugs): faults.append(f"docs/platforms/{slug}/ documents a platform that is not registered") + faults.extend(prefix_faults(rows)) faults.extend(unlisted_headers(rows)) faults.extend(naming_faults(rows, vocabulary(rows))) faults.extend(role_faults()) @@ -305,7 +346,11 @@ def check(): sys.exit(1) for path, term, reason in ALLOWED: print(f"allowed: {path} may say {term} — {reason}") + for token, prefixes in sorted(CLASS_PREFIXES.items()): + spellings = ", ".join(f"SolidSyslog{prefix}*" for prefix in prefixes) + print(f"allowed: {token} may also declare {spellings} — a second upstream, agreed when it was taken on") print( - f"docs match the code: {len(registered())} platforms, all documented and none naming " - f"another; {len(declared_roles())} roles, each listed everywhere roles are enumerated" + f"docs match the code: {len(registered())} platforms, all documented, none naming " + f"another and each declaring only classes that carry its token; " + f"{len(declared_roles())} roles, each listed everywhere roles are enumerated" ) From f45c4de0711aed8abc7c4e6626968e16b61df853 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 07:08:58 +0100 Subject: [PATCH 2/2] ci: state the alias in the check that permits it The prefix_faults docstring stated the token-only rule in the one place that accepts a second-upstream prefix. Iterable unpacking for both alias lists, so the file spells the idiom one way (Ruff RUF005). Part of #708 --- scripts/check_platform_docs.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/check_platform_docs.py b/scripts/check_platform_docs.py index eeea4dc6..89db9869 100644 --- a/scripts/check_platform_docs.py +++ b/scripts/check_platform_docs.py @@ -150,7 +150,7 @@ def vocabulary(rows): """ terms = {} for token, directory in rows: - for alias in [token] + ALIASES.get(token, []): + for alias in [token, *ALIASES.get(token, [])]: terms.setdefault(alias, set()).add(token) for header in interface_headers(directory): stem = header[: -len(".h")] @@ -261,7 +261,8 @@ def role_faults(): def prefix_faults(rows): - """Every header a platform declares begins with SolidSyslog. + """Every header a platform declares begins with SolidSyslog, or with + an agreed second-upstream prefix from CLASS_PREFIXES. docs/NAMING.md states this for public classes. An *Errors.h takes its name from the class it belongs to, so it carries the token by construction and is @@ -269,7 +270,7 @@ def prefix_faults(rows): """ faults = [] for token, directory in rows: - accepted = [token] + CLASS_PREFIXES.get(token, []) + accepted = [token, *CLASS_PREFIXES.get(token, [])] for header in interface_headers(directory): if not any(header.startswith(f"SolidSyslog{prefix}") for prefix in accepted): wanted = " or ".join(f"SolidSyslog{prefix}" for prefix in accepted)