ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
fix_wave_references.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_wave_references.py -- conservative auto-fix for "Wave N" session refs.
5
6Companion to check_no_wave_references.py. Walks every tracked source/doc
7file under the same scan roots and rewrites obvious session-bookkeeping
8patterns:
9
10 - "(Wave 70)" -> "" (delete the parenthetical)
11 - "[wave-3]" -> "" (delete the bracketed)
12 - " -- Wave 12" -> "" (trailing em-dash citation)
13 - "see Wave N" -> "see HUM" (placeholder cite; human reviews)
14 - "Wave 70 added X" -> "X" (drop the leading attribution)
15 - "Wave 70 fixed X" -> "Fixed: X"
16 - "Wave 70's X" -> "the X"
17 - "the wave 70 X" -> "the X"
18 - "during Wave 11" -> ""
19 - "post-wave-7" -> ""
20 - "from Wave N" -> ""
21
22Anything else that the gate flagged but this script could not rewrite is
23reported on stderr so a human can fix it manually.
24
25Usage:
26 scripts/fix/fix_wave_references.py # dry-run (default)
27 scripts/fix/fix_wave_references.py --apply # write changes back
28
29Exit code:
30 0 -- no remaining violations after the rewrite (or none to start with)
31 1 -- some violations remain that the script could not rewrite
32
33@copyright Copyright (c) 2026 Brighton Sikarskie
34SPDX-License-Identifier: MIT
35"""
36
37from __future__ import annotations
38
39import argparse
40import os
41import re
42import sys
43from pathlib import Path
44
45# Maximum line length shown in the remaining-violations report.
46REPORT_SNIPPET_MAX_LEN = 120
47# Maximum number of violations printed before showing a count summary.
48REPORT_MAX_LINES = 50
49
50SCAN_ROOTS = (
51 "libs",
52 "examples",
53 "tests",
54 "port",
55 "scripts",
56 "docs",
57 "cmake",
58 ".github",
59)
60SKIP_DIR_NAMES = frozenset(
61 {
62 "build",
63 "build-cov",
64 "build-scan",
65 "build-tidy",
66 ".git",
67 "_deps",
68 "third_party",
69 "__pycache__",
70 ".cache",
71 "node_modules",
72 "reference",
73 }
74)
75SCAN_EXTS = frozenset(
76 {
77 ".c",
78 ".h",
79 ".cpp",
80 ".hpp",
81 ".cc",
82 ".cmake",
83 ".md",
84 ".yml",
85 ".yaml",
86 ".sh",
87 ".py",
88 ".txt",
89 }
90)
91SCAN_BASENAMES = frozenset({"justfile", "Dockerfile", "CMakeLists.txt"})
92
93SELF_EXEMPT = frozenset(
94 {
95 "scripts/checks/check_no_wave_references.py",
96 "scripts/fix/fix_wave_references.py",
97 "docs/STYLE_GUIDE.md",
98 "CLAUDE.md",
99 }
100)
101
102# Ordered: most-specific first so we strip larger phrases before residue.
103REWRITES: list[tuple[re.Pattern[str], str]] = [
104 # "(Wave 70)" or "[wave-7]" -- entire parenthetical/bracketed.
105 (re.compile(r"\s*[\‍(\‍[][Ww]ave[\s_\-]?\d+[A-Za-z]?[\‍)\‍]]"), ""),
106 # " -- Wave 12 ..." trailing comment citation up to end-of-line/period.
107 (re.compile(r"\s*--\s*[Ww]ave[\s_\-]?\d+[A-Za-z]?[^.\n]*"), ""),
108 # "; see Wave N" / ", see Wave N" -- inline see-also citation.
109 (re.compile(r"[,;]\s*see\s+[Ww]ave[\s_\-]?\d+[A-Za-z]?", re.IGNORECASE), ""),
110 # "see Wave N" -> "see the relevant HUM section" placeholder.
111 (
112 re.compile(r"\bsee\s+[Ww]ave[\s_\-]?\d+[A-Za-z]?\b", re.IGNORECASE),
113 "see the relevant HUM section",
114 ),
115 # "the wave-N <thing>" -> "the <thing>"
116 (re.compile(r"\bthe\s+[Ww]ave[\s_\-]?\d+[A-Za-z]?\s+", re.IGNORECASE), "the "),
117 # "during Wave N" / "in Wave N" / "from Wave N" / "after Wave N" / "post-wave-N"
118 (
119 re.compile(
120 r"\b(?:during|in|from|after|post[\s_\-]?)[Ww]ave[\s_\-]?\d+[A-Za-z]?\b", re.IGNORECASE
121 ),
122 "",
123 ),
124 # "pre-Wave-N" -> "previous"
125 (re.compile(r"\bpre[\s_\-]?[Ww]ave[\s_\-]?\d+[A-Za-z]?\b", re.IGNORECASE), "previous"),
126 # "Wave N's <thing>" -> "the <thing>" (handles possessive)
127 (re.compile(r"\b[Ww]ave[\s_\-]?\d+[A-Za-z]?'s\b"), "the"),
128 # "Wave N: " sentence prefix -> ""
129 (re.compile(r"^\s*[\*\#\-/\s]*[Ww]ave[\s_\-]?\d+[A-Za-z]?\s*:\s*", re.MULTILINE), ""),
130 # "Wave N added X" / "Wave N fixed X" -> drop the "Wave N " prefix.
131 (
132 re.compile(
133 r"\b[Ww]ave[\s_\-]?\d+[A-Za-z]?\s+(added|fixed|introduced|removed|"
134 r"refactored|reworked|landed|backfilled|swept|cleaned|wired|spawned|"
135 r"split|merged|deleted|created|reverted|enabled|disabled|brought\s+up)\b",
136 re.IGNORECASE,
137 ),
138 r"\1",
139 ),
140 # Bare "Wave N" left over -- last resort, just delete the token + one
141 # trailing space.
142 (re.compile(r"\b[Ww]ave[\s_\-]?\d+[A-Za-z]?\s?"), ""),
143]
144
145WAVE_RE = re.compile(r"\b[Ww]ave[\s_\-]?\d+[A-Za-z]?\b")
146
147
148def _is_skip_dir(name: str) -> bool:
149 """Whether a directory name is build output or otherwise out of scope.
150
151 Matches the ``build-*`` family by prefix as well as the exact names, so a
152 CMake variant directory (build-cov, build-fuzz) is skipped without being
153 listed individually.
154 """
155 return name in SKIP_DIR_NAMES or name == "build" or name.startswith("build-")
156
157
158def should_scan(p: Path) -> bool:
159 """Whether this file's name or suffix puts it in scope.
160
161 Basename is checked as well as suffix so extensionless files the tree
162 cares about (CMakeLists.txt, justfile) are not missed.
163 """
164 return p.name in SCAN_BASENAMES or p.suffix in SCAN_EXTS
165
166
167def iter_files(root: Path) -> list[Path]:
168 """Every in-scope file beneath the configured scan roots."""
169 out: list[Path] = []
170 for sr in SCAN_ROOTS:
171 base = root / sr
172 if not base.exists():
173 continue
174 for dp, dn, fn in os.walk(base):
175 dn[:] = [d for d in dn if not _is_skip_dir(d)]
176 for f in fn:
177 p = Path(dp) / f
178 if should_scan(p):
179 out.append(p)
180 for top in ("justfile", "CMakeLists.txt", "README.md"):
181 p = root / top
182 if p.exists():
183 out.append(p)
184 return out
185
186
187def rewrite_line(line: str) -> str:
188 """Delete session-bookkeeping "Wave N" references from one line.
189
190 Whitespace cleanup is deliberately conservative -- only the trailing and
191 doubled spaces the deletion itself introduced. These references sit inside
192 prose, and reflowing the surrounding sentence would produce a diff far
193 larger than the edit being made.
194 """
195 for rx, sub in REWRITES:
196 line = rx.sub(sub, line)
197 # Conservative: only clean up trailing whitespace and double-spaces
198 # introduced by deletions WITHIN this line.
199 return re.sub(r" +", " ", line).rstrip()
200
201
202def rewrite(text: str) -> str:
203 """Rewrite only the lines naming a wave, leaving the rest byte-identical."""
204 out = []
205 for ln in text.splitlines():
206 if WAVE_RE.search(ln):
207 out.append(rewrite_line(ln))
208 else:
209 out.append(ln)
210 return "\n".join(out) + ("\n" if text.endswith("\n") else "")
211
212
213def main() -> int:
214 """Report, or with ``--apply`` perform, the wave-reference deletion.
215
216 Dry-run by default. Returns 0 when nothing needed rewriting and 1 when
217 something did, so the dry run doubles as the gate.
218 """
219 ap = argparse.ArgumentParser()
220 ap.add_argument("--apply", action="store_true", help="write changes back (default: dry-run)")
221 args = ap.parse_args()
222
223 root = Path(__file__).resolve().parents[2]
224 files = iter_files(root)
225
226 fixed_files = 0
227 fixed_lines = 0
228 remaining: list[tuple[Path, int, str]] = []
229
230 for path in files:
231 rel = str(path.relative_to(root))
232 if rel in SELF_EXEMPT:
233 continue
234 try:
235 old = path.read_text(encoding="utf-8")
236 except (OSError, UnicodeDecodeError):
237 continue
238 if not WAVE_RE.search(old):
239 continue
240 new = rewrite(old)
241 if new != old:
242 old_lines = old.splitlines()
243 new_lines = new.splitlines()
244 n_changed = sum(1 for a, b in zip(old_lines, new_lines, strict=False) if a != b)
245 n_changed += abs(len(old_lines) - len(new_lines))
246 fixed_files += 1
247 fixed_lines += n_changed
248 if args.apply:
249 path.write_text(new, encoding="utf-8")
250 # Re-scan for residue after the in-memory rewrite.
251 for ln, line in enumerate(new.splitlines(), start=1):
252 if "WAVE-OK" in line:
253 continue
254 if WAVE_RE.search(line):
255 remaining.append((path.relative_to(root), ln, line.rstrip()))
256
257 mode = "applied" if args.apply else "dry-run"
258 print(f"fix-wave-refs ({mode}): rewrote {fixed_lines} lines across {fixed_files} files.")
259 if remaining:
260 print(
261 f"REMAINING: {len(remaining)} unfixable violations -- needs human edit:",
262 file=sys.stderr,
263 )
264 for rel, ln, line in remaining[:REPORT_MAX_LINES]:
265 snippet = line if len(line) <= REPORT_SNIPPET_MAX_LEN else line[:117] + "..."
266 print(f" {rel}:{ln} {snippet}", file=sys.stderr)
267 if len(remaining) > REPORT_MAX_LINES:
268 print(f" ... {len(remaining) - REPORT_MAX_LINES} more", file=sys.stderr)
269 return 1
270 return 0
271
272
273if __name__ == "__main__":
274 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.