Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions docs/NAMING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 56 additions & 10 deletions scripts/check_platform_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand All @@ -128,14 +150,12 @@ 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)
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


Expand Down Expand Up @@ -240,6 +260,27 @@ def role_faults():
return faults


def prefix_faults(rows):
"""Every header a platform declares begins with SolidSyslog<Token>, 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
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
Comment thread
DavidCozens marked this conversation as resolved.


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
Expand Down Expand Up @@ -286,6 +327,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())
Expand All @@ -305,7 +347,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"
)
Loading