ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
fix_inclusive_terminology.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""fix_inclusive_terminology.py -- conservative auto-fix for legacy spellings.
5
6Companion to check_inclusive_terminology.py. Walks tracked source/docs and
7rewrites the most common comment/identifier patterns to their inclusive
8counterparts. Anything the script cannot rewrite is reported on stderr for
9a human to review.
10
11Substitutions (case-preserving where possible):
12
13 Plain words (in comments / docs):
14 master -> primary Master -> Primary
15 masters -> primaries Masters -> Primaries
16 mastered -> finalised (rare; flagged for review)
17 slave -> peripheral Slave -> Peripheral
18 slaves -> peripherals Slaves -> Peripherals
19 slaved -> bound (rare; flagged for review)
20
21 Phrases:
22 master/slave -> controller/peripheral
23 master-slave -> controller-peripheral
24 Slave Select / SS pin -> Chip Select / CS pin
25 MOSI -> COPI
26 MISO -> CIPO
27 slave stack -> device stack
28 slave-state machine -> device-state machine
29 slave device -> device
30
31This script intentionally leaves UPSTREAM SYMBOLS alone -- e.g.
32``UX_SLAVE_TRANSFER``, ``MBEDTLS_SSL_EXTENDED_MASTER_SECRET``,
33``r_iic_b_master_open`` -- because those are part of the upstream API
34contract. Use the per-line ``LEGACY-OK: <reason>`` opt-out for those.
35
36Usage:
37 scripts/fix/fix_inclusive_terminology.py # dry-run
38 scripts/fix/fix_inclusive_terminology.py --apply # write back
39
40Exit code:
41 0 -- no remaining violations
42 1 -- some remain (human edit needed)
43
44@copyright Copyright (c) 2026 Brighton Sikarskie
45SPDX-License-Identifier: MIT
46"""
47
48from __future__ import annotations
49
50import argparse
51import os
52import re
53import sys
54from pathlib import Path
55
56# Maximum line length shown in the remaining-violations report.
57REPORT_SNIPPET_MAX_LEN = 120
58# Maximum number of violations printed before showing a count summary.
59REPORT_MAX_LINES = 50
60
61SCAN_ROOTS = (
62 "libs",
63 "examples",
64 "tests",
65 "port",
66 "scripts",
67 "docs",
68 "cmake",
69 ".github",
70)
71SKIP_DIR_NAMES = frozenset(
72 {
73 "build",
74 "build-cov",
75 "build-scan",
76 "build-tidy",
77 ".git",
78 "_deps",
79 "third_party",
80 "__pycache__",
81 ".cache",
82 "node_modules",
83 "reference",
84 }
85)
86SCAN_EXTS = frozenset(
87 {
88 ".c",
89 ".h",
90 ".cpp",
91 ".hpp",
92 ".cc",
93 ".cmake",
94 ".md",
95 ".yml",
96 ".yaml",
97 ".sh",
98 ".py",
99 ".txt",
100 }
101)
102SCAN_BASENAMES = frozenset({"justfile", "Dockerfile", "CMakeLists.txt"})
103
104# Mirror the gate's exemption / skip list so we do not rewrite vendor
105# files that the gate ignores.
106SELF_EXEMPT = frozenset(
107 {
108 "scripts/checks/check_inclusive_terminology.py",
109 "scripts/fix/fix_inclusive_terminology.py",
110 "docs/STYLE_GUIDE.md",
111 "docs/RING_AND_WORLD.md",
112 "docs/ACRONYMS.md",
113 "docs/MCDC_GAPS.md",
114 "docs/MCDC_GAPS.csv",
115 "CLAUDE.md",
116 ".github/workflows/inclusive-terminology.yml",
117 }
118)
119
120# These paths are documented vendor citations; leave them alone.
121SKIP_PATTERNS = frozenset(
122 {
123 "libs/ra8_hal/inc/ra8_iic_b_regs.h",
124 "libs/ra8_hal/inc/ra8_i3c_regs.h",
125 "libs/ra8_hal/inc/ra8_ospi_regs.h",
126 "libs/ra8_hal/inc/ra8_mipi_phy_regs.h",
127 "libs/ra8_hal/inc/ra8_spi_regs.h",
128 "libs/ra8_hal/inc/ra8_ssie_regs.h",
129 "libs/ra8_hal/inc/ra8_vin_regs.h",
130 "libs/ra8_hal/inc/ra8_vreg_regs.h",
131 "libs/ra8_hal/inc/ra8_iic_b.h",
132 "libs/ra8_hal/src/ra8_iic_b.c",
133 "libs/ra8_hal/inc/ra8_i2c.h",
134 "libs/ra8_hal/src/ra8_i2c.c",
135 "libs/ra8_hal/inc/ra8_mipi_phy.h",
136 "libs/ra8_hal/src/ra8_mipi_phy.c",
137 "tests/hal/src/test_ra8_mipi_phy_init.c",
138 "tests/hal/src/test_ra8_mipi_phy_lanes.c",
139 "docs/SOUP/nimble.md",
140 }
141)
142
143# Ordered: most-specific first.
144REWRITES: list[tuple[re.Pattern[str], str]] = [
145 (re.compile(r"\bmaster[/\-]slave\b"), "controller-peripheral"),
146 (re.compile(r"\bMaster[/\-]Slave\b"), "Controller-Peripheral"),
147 (re.compile(r"\bMOSI\b"), "COPI"),
148 (re.compile(r"\bMISO\b"), "CIPO"),
149 (re.compile(r"\bSlave[ _\-]Select\b", re.IGNORECASE), "Chip Select"),
150 (re.compile(r"\bSS\s+pin\b"), "CS pin"),
151 (re.compile(r"\bslave\s+stack\b", re.IGNORECASE), "device stack"),
152 (re.compile(r"\bslave[ _\-]state\s+machine\b", re.IGNORECASE), "device-state machine"),
153 (re.compile(r"\bslave\s+device\b", re.IGNORECASE), "device"),
154 (re.compile(r"\bSlave\s+Device\b"), "Device"),
155 (re.compile(r"\bSLAVE\b"), "PERIPHERAL"),
156 (re.compile(r"\bMASTER\b"), "PRIMARY"),
157 (re.compile(r"\bMasters\b"), "Primaries"),
158 (re.compile(r"\bmasters\b"), "primaries"),
159 (re.compile(r"\bMaster\b"), "Primary"),
160 (re.compile(r"\bmaster\b"), "primary"),
161 (re.compile(r"\bSlaves\b"), "Peripherals"),
162 (re.compile(r"\bslaves\b"), "peripherals"),
163 (re.compile(r"\bSlave\b"), "Peripheral"),
164 (re.compile(r"\bslave\b"), "peripheral"),
165]
166
167# Same regex the gate uses to count residue.
168LEGACY_RE = re.compile(
169 r"(?<![A-Za-z0-9_])(master|slave|MOSI|MISO)(?![A-Za-z0-9_])",
170 re.IGNORECASE,
171)
172OPTOUT_RE = re.compile(r"LEGACY-OK\s*:")
173
174
175def _is_skip_dir(name: str) -> bool:
176 """Whether a directory name is build output or otherwise out of scope.
177
178 Matches the ``build-*`` family by prefix as well as the exact names, so a
179 CMake variant directory (build-cov, build-fuzz) is skipped without being
180 listed individually.
181 """
182 return name in SKIP_DIR_NAMES or name == "build" or name.startswith("build-")
183
184
185def should_scan(p: Path) -> bool:
186 """Whether this file's name or suffix puts it in scope.
187
188 Basename is checked as well as suffix so extensionless files the tree
189 cares about (CMakeLists.txt, justfile) are not missed.
190 """
191 return p.name in SCAN_BASENAMES or p.suffix in SCAN_EXTS
192
193
194def iter_files(root: Path) -> list[Path]:
195 """Every in-scope file beneath the configured scan roots."""
196 out: list[Path] = []
197 for sr in SCAN_ROOTS:
198 base = root / sr
199 if not base.exists():
200 continue
201 for dp, dn, fn in os.walk(base):
202 dn[:] = [d for d in dn if not _is_skip_dir(d)]
203 for f in fn:
204 p = Path(dp) / f
205 if should_scan(p):
206 out.append(p)
207 for top in ("justfile", "CMakeLists.txt", "README.md"):
208 p = root / top
209 if p.exists():
210 out.append(p)
211 return out
212
213
214def rewrite_line(line: str) -> str:
215 """Apply every terminology rewrite to one line and tidy the result.
216
217 Collapses runs of spaces afterwards, because the replacements differ in
218 length from what they replace and would otherwise leave ragged gaps where
219 a longer legacy term was swapped out.
220 """
221 for rx, sub in REWRITES:
222 line = rx.sub(sub, line)
223 return re.sub(r" +", " ", line).rstrip()
224
225
226def rewrite(text: str) -> str:
227 """Rewrite only the lines carrying legacy terminology, leaving the rest byte-identical.
228
229 Lines matching the opt-out marker are passed through untouched -- that is
230 what protects a deliberate quotation of a vendor document, where the
231 legacy term is the accurate one.
232 """
233 out = []
234 for ln in text.splitlines():
235 if LEGACY_RE.search(ln) and not OPTOUT_RE.search(ln):
236 out.append(rewrite_line(ln))
237 else:
238 out.append(ln)
239 return "\n".join(out) + ("\n" if text.endswith("\n") else "")
240
241
242def main() -> int:
243 """Report, or with ``--apply`` perform, the inclusive-terminology rewrite.
244
245 Dry-run by default: without ``--apply`` nothing is written, so the diff
246 can be inspected before a tree-wide substitution is committed.
247
248 Returns 0 when no line needed rewriting, 1 when some did (in either mode),
249 so the dry run doubles as a gate.
250 """
251 ap = argparse.ArgumentParser()
252 ap.add_argument("--apply", action="store_true")
253 args = ap.parse_args()
254
255 root = Path(__file__).resolve().parents[2]
256 files = iter_files(root)
257
258 fixed_files = 0
259 fixed_lines = 0
260 remaining: list[tuple[Path, int, str]] = []
261
262 for path in files:
263 rel = str(path.relative_to(root))
264 if rel in SELF_EXEMPT or rel in SKIP_PATTERNS:
265 continue
266 try:
267 old = path.read_text(encoding="utf-8")
268 except (OSError, UnicodeDecodeError):
269 continue
270 if not LEGACY_RE.search(old):
271 continue
272 new = rewrite(old)
273 if new != old:
274 n = sum(1 for a, b in zip(old.splitlines(), new.splitlines(), strict=False) if a != b)
275 n += abs(len(old.splitlines()) - len(new.splitlines()))
276 fixed_files += 1
277 fixed_lines += n
278 if args.apply:
279 path.write_text(new, encoding="utf-8")
280 for ln, line in enumerate(new.splitlines(), start=1):
281 if OPTOUT_RE.search(line):
282 continue
283 if LEGACY_RE.search(line):
284 remaining.append((path.relative_to(root), ln, line.rstrip()))
285
286 mode = "applied" if args.apply else "dry-run"
287 print(f"fix-inclusive ({mode}): rewrote {fixed_lines} lines across {fixed_files} files.")
288 if remaining:
289 print(f"REMAINING: {len(remaining)} -- needs human edit:", file=sys.stderr)
290 for rel, ln, line in remaining[:REPORT_MAX_LINES]:
291 snippet = line if len(line) <= REPORT_SNIPPET_MAX_LEN else line[:117] + "..."
292 print(f" {rel}:{ln} {snippet}", file=sys.stderr)
293 if len(remaining) > REPORT_MAX_LINES:
294 print(f" ... {len(remaining) - REPORT_MAX_LINES} more", file=sys.stderr)
295 return 1
296 return 0
297
298
299if __name__ == "__main__":
300 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298
int abs(int j)
Compute absolute value of integer.