4"""fix_inclusive_terminology.py -- conservative auto-fix for legacy spellings.
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
11Substitutions (case-preserving where possible):
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)
22 master/slave -> controller/peripheral
23 master-slave -> controller-peripheral
24 Slave Select / SS pin -> Chip Select / CS pin
27 slave stack -> device stack
28 slave-state machine -> device-state machine
29 slave device -> device
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.
37 scripts/fix/fix_inclusive_terminology.py # dry-run
38 scripts/fix/fix_inclusive_terminology.py --apply # write back
41 0 -- no remaining violations
42 1 -- some remain (human edit needed)
44@copyright Copyright (c) 2026 Brighton Sikarskie
45SPDX-License-Identifier: MIT
48from __future__
import annotations
54from pathlib
import Path
57REPORT_SNIPPET_MAX_LEN = 120
71SKIP_DIR_NAMES = frozenset(
102SCAN_BASENAMES = frozenset({
"justfile",
"Dockerfile",
"CMakeLists.txt"})
106SELF_EXEMPT = frozenset(
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",
114 "docs/MCDC_GAPS.csv",
116 ".github/workflows/inclusive-terminology.yml",
121SKIP_PATTERNS = frozenset(
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",
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"),
168LEGACY_RE = re.compile(
169 r"(?<![A-Za-z0-9_])(master|slave|MOSI|MISO)(?![A-Za-z0-9_])",
172OPTOUT_RE = re.compile(
r"LEGACY-OK\s*:")
175def _is_skip_dir(name: str) -> bool:
176 """Whether a directory name is build output or otherwise out of scope.
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
182 return name
in SKIP_DIR_NAMES
or name ==
"build" or name.startswith(
"build-")
185def should_scan(p: Path) -> bool:
186 """Whether this file's name or suffix puts it in scope.
188 Basename is checked as well as suffix so extensionless files the tree
189 cares about (CMakeLists.txt, justfile) are not missed.
191 return p.name
in SCAN_BASENAMES
or p.suffix
in SCAN_EXTS
194def iter_files(root: Path) -> list[Path]:
195 """Every in-scope file beneath the configured scan roots."""
197 for sr
in SCAN_ROOTS:
199 if not base.exists():
201 for dp, dn, fn
in os.walk(base):
202 dn[:] = [d
for d
in dn
if not _is_skip_dir(d)]
207 for top
in (
"justfile",
"CMakeLists.txt",
"README.md"):
214def rewrite_line(line: str) -> str:
215 """Apply every terminology rewrite to one line and tidy the result.
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.
221 for rx, sub
in REWRITES:
222 line = rx.sub(sub, line)
223 return re.sub(
r" +",
" ", line).rstrip()
226def rewrite(text: str) -> str:
227 """Rewrite only the lines carrying legacy terminology, leaving the rest byte-identical.
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.
234 for ln
in text.splitlines():
235 if LEGACY_RE.search(ln)
and not OPTOUT_RE.search(ln):
236 out.append(rewrite_line(ln))
239 return "\n".join(out) + (
"\n" if text.endswith(
"\n")
else "")
243 """Report, or with ``--apply`` perform, the inclusive-terminology rewrite.
245 Dry-run by default: without ``--apply`` nothing is written, so the diff
246 can be inspected before a tree-wide substitution is committed.
248 Returns 0 when no line needed rewriting, 1 when some did (in either mode),
249 so the dry run doubles as a gate.
251 ap = argparse.ArgumentParser()
252 ap.add_argument(
"--apply", action=
"store_true")
253 args = ap.parse_args()
255 root = Path(__file__).resolve().parents[2]
256 files = iter_files(root)
260 remaining: list[tuple[Path, int, str]] = []
263 rel = str(path.relative_to(root))
264 if rel
in SELF_EXEMPT
or rel
in SKIP_PATTERNS:
267 old = path.read_text(encoding=
"utf-8")
268 except (OSError, UnicodeDecodeError):
270 if not LEGACY_RE.search(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()))
279 path.write_text(new, encoding=
"utf-8")
280 for ln, line
in enumerate(new.splitlines(), start=1):
281 if OPTOUT_RE.search(line):
283 if LEGACY_RE.search(line):
284 remaining.append((path.relative_to(root), ln, line.rstrip()))
286 mode =
"applied" if args.apply
else "dry-run"
287 print(f
"fix-inclusive ({mode}): rewrote {fixed_lines} lines across {fixed_files} files.")
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)
299if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.
int abs(int j)
Compute absolute value of integer.