4r"""check_no_wave_references.py -- ban session-bookkeeping "Wave N" references.
6Rationale: comments and commit messages that cite "Wave 70 fixed FRDY" or
7"see Wave 43b" leak internal session bookkeeping into the source tree.
8Future readers do not care WHEN a fix was found, only WHY. Reference the
9function or symbol or HUM section instead.
11What this gate flags (case-insensitive):
12 - "Wave 70", "wave 3", "WAVE-12", "wave_43b" -- a "wave" token immediately
13 followed (after at most one [\\s_-]) by a digit. Optional trailing
14 letter(s) for sub-numbering ("wave 43b").
15 - "(Wave 12)" / "[wave-7]" -- same pattern wrapped in punctuation.
17What this gate does NOT flag (legitimate domain usage):
18 - "waveform", "wave shape", "saw wave", "sine wave" (no digit follows).
19 - Identifiers like "k_ra8_pdg_wave_saw", "wave_select", "wave_table".
20 - Renesas / MIPI / vendor symbols that happen to contain "wave".
22Per-line opt-out: append "WAVE-OK: <reason>" on the offending line. Reserve
23for unavoidable upstream-symbol citations (e.g. a Renesas register name
24that literally encodes the wording).
26Scope is DERIVED from ``git ls-files`` via ``lint_targets.first_party_paths``
27rather than a hardcoded root list, so a new top-level directory (``tools/``,
28``coprocessor/``, ``infra/`` and ``just/`` were the ones the old list silently
29omitted, #549) is covered the day it lands. ``--selftest`` proves the detector
30fires and stays quiet, and that the derived scope clears its floor.
35 2 -- the scope collapsed below FILE_FLOOR (a scan of almost nothing)
37@copyright Copyright (c) 2026 Brighton Sikarskie
38SPDX-License-Identifier: MIT
41from __future__
import annotations
45from pathlib
import Path
47sys.path.insert(0, str(Path(__file__).resolve().parent))
49from lint_targets
import first_party_paths
50from selftest_assert
import expect, report
52SCAN_EXTS: frozenset[str] = frozenset(
71SCAN_BASENAMES: frozenset[str] = frozenset(
72 {
"justfile",
"Dockerfile",
"CMakeLists.txt",
"GNUmakefile",
"Justfile"}
81DOCS_VENDOR_DIRS: frozenset[str] = frozenset({
"reference",
"doxygen",
"html"})
91MAX_FINDINGS_SHOWN = 50
95SNIPPET_TRIM_LEN = SNIPPET_MAX_LEN - 3
99WAVE_RE: re.Pattern[str] = re.compile(
r"\b[Ww]ave[\s_\-]?\d+[A-Za-z]?\b")
101OPTOUT_RE: re.Pattern[str] = re.compile(
r"WAVE-OK\s*:")
103SELF_EXEMPT_FILES: frozenset[str] = frozenset(
105 "scripts/checks/check_no_wave_references.py",
106 "scripts/fix/fix_wave_references.py",
107 "docs/STYLE_GUIDE.md",
113def iter_source_files(root: Path) -> list[Path]:
114 """Every in-scope first-party file, derived from git rather than a root list.
116 Enumeration goes through ``lint_targets.first_party_paths`` -- the shared
117 derived-scope primitive -- so ``tools/``, ``coprocessor/``, ``infra/`` and
118 ``just/`` (the roots a hardcoded list silently dropped, #549) are covered. The
119 only subtractions on top of what that primitive already exempts are the
120 docs-side vendored/generated directories in ``DOCS_VENDOR_DIRS``.
122 rels = set(first_party_paths(tuple(SCAN_EXTS)))
123 for name
in SCAN_BASENAMES:
124 rels |= {rel
for rel
in first_party_paths((name,))
if Path(rel).name == name}
126 for rel
in sorted(rels):
127 if set(Path(rel).parts) & DOCS_VENDOR_DIRS:
129 out.append(root / rel)
133def scan_file(path: Path, root: Path) -> list[tuple[Path, int, str]]:
134 """Report every "Wave N" session reference in one file.
136 Self-exempt files are skipped whole: this checker and the policy doc must
137 spell the banned pattern to describe it, and tagging every such line
138 individually would bury them.
140 rel = path.relative_to(root)
141 if str(rel)
in SELF_EXEMPT_FILES:
144 text = path.read_text(encoding=
"utf-8")
145 except (OSError, UnicodeDecodeError):
147 out: list[tuple[Path, int, str]] = []
148 for lineno, line
in enumerate(text.splitlines(), start=1):
149 if OPTOUT_RE.search(line):
151 if WAVE_RE.search(line):
152 out.append((rel, lineno, line.rstrip()))
156def selftest() -> int:
157 """Prove the detector fires on a "Wave N" tag, stays quiet otherwise, and scans real files.
159 Both directions plus a scope probe: the numbered-session pattern must FIRE,
160 the legitimate domain uses (``waveform``, ``sine wave``, ``wave_table``)
161 must stay QUIET, the derived scope must clear ``FILE_FLOOR``, and it must
162 reach the roots a hardcoded list had dropped (``infra/``, ``just/``) -- a
163 clean run over a scope that never sees those roots proves nothing.
166 0 when every assertion held in both directions, 1 otherwise.
168 failures: list[str] = []
169 for text, must_fire, label
in (
170 (
"fixed in Wave 70",
True,
'MUST FIRE: "Wave 70"'),
171 (
"see wave-43b for context",
True,
'MUST FIRE: "wave-43b"'),
172 (
"the sine wave is smooth",
False,
'MUST NOT FIRE: "sine wave"'),
173 (
"k_ra8_pdg_wave_saw selects the waveform",
False,
"MUST NOT FIRE: wave_saw / waveform"),
174 (
"wave_table[0] holds the sample",
False,
"MUST NOT FIRE: wave_table identifier"),
176 fired = bool(WAVE_RE.search(text))
177 expect(fired == must_fire, label, failures)
179 root = Path(__file__).resolve().parents[2]
180 files = iter_source_files(root)
181 rels = {str(p.relative_to(root))
for p
in files
if p.is_relative_to(root)}
183 len(files) >= FILE_FLOOR,
184 f
"derived scope sees {len(files)} file(s) (floor {FILE_FLOOR})",
187 for root_name
in (
"infra",
"just"):
189 any(rel.startswith(root_name +
"/")
for rel
in rels),
190 f
"the derived scope reaches {root_name}/ (previously omitted)",
193 return report(failures)
197 """Ban session-bookkeeping "Wave N" references from the tree.
199 These leak an internal working chronology into source that outlives it: a
200 future reader cannot resolve "see Wave 43b" to anything, whereas the
201 function, symbol or HUM section the fix touched stays findable.
203 Returns 1 listing each reference, 0 when the tree is clean, 2 when the
204 derived scope collapsed below FILE_FLOOR.
206 if "--selftest" in sys.argv[1:]:
208 root = Path(__file__).resolve().parents[2]
209 files = iter_source_files(root)
210 if len(files) < FILE_FLOOR:
212 f
"check_no_wave_references.py: FATAL -- only {len(files)} file(s) in scope, "
213 f
"floor is {FILE_FLOOR}. A collapsed scope reports a clean tree because it "
217 findings: list[tuple[Path, int, str]] = []
219 findings.extend(scan_file(f, root))
222 print(
"no-wave-refs: 0 violations -- gate clean.")
225 print(f
"no-wave-refs: {len(findings)} violations found.")
226 for rel, lineno, line
in findings[:MAX_FINDINGS_SHOWN]:
227 snippet = line
if len(line) <= SNIPPET_MAX_LEN
else line[:SNIPPET_TRIM_LEN] +
"..."
228 print(f
" {rel}:{lineno} {snippet}")
229 if len(findings) > MAX_FINDINGS_SHOWN:
230 print(f
" ... {len(findings) - MAX_FINDINGS_SHOWN} more (truncated)")
232 print(
'Per-line opt-out: append "WAVE-OK: <reason>" on the offending line.')
233 print(
"Auto-fix helper: scripts/fix/fix_wave_references.py --apply")
237if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.