4"""Gate + fixer: house style for single-line C/C++ block comments.
6clang-format owns the *start* column of trailing comments (``AlignTrailing
7Comments: true`` aligns each block's ``/**<`` to the widest code + one space)
8but never touches the comment *interior* (``ReflowComments: false``). This
9pass runs *after* clang-format and owns the interior, normalising three things:
111. **Space after the opener.** ``/*foo`` -> ``/* foo`` (the Doxygen forms
12 ``/**`` / ``/**<`` likewise gain a single space before their text).
132. **Space before the closer.** ``foo.*/`` -> ``foo. */`` -- one space before
14 ``*/`` unless the comment is widened for end alignment (rule 3).
153. **Aligned ``*/`` end column.** Across a run of consecutive trailing comments
16 that clang-format placed in the *same start column*, the closers are padded
17 to line up under the longest (which gets one space before ``*/``). A run is
18 broken by a blank line, a code-only line, a standalone comment, a change of
19 start column, or where padding would reach the column limit (which would make
20 clang-format collapse its leading alignment). Standalone full-line comments
21 are spacing-normalised but never end-aligned.
224. **One block, one alignment.** Rule 3 *accommodates* a torn block; this rule
23 forbids one. ``AlignTrailingComments`` aligns a run to its widest code plus
24 one space but abandons that column when the longest comment would cross the
25 limit, leaving one struct with two ``/**<`` columns and two ``*/`` columns.
26 That is canonical clang-format output, so neither tool objects to it, and
27 ragged blocks accumulated in the tree unremarked. A run must therefore align
28 as a single unit. This rule is REPORTED, never fixed: the remedy is to
29 shorten the comment or move it to its own ``/** ... */`` block above the line
30 it documents, and neither is a rewrite a formatter may make on its own. A
31 reported block IS exempted from rule 3, though -- end-padding is a grouping
32 hint clang-format honours, so padding a torn block to its two sub-widths
33 would keep it torn even after the author shortened the comment that tore it.
35The text in front of each comment -- code plus clang-format's leading alignment
36-- is preserved verbatim, so the two tools never fight: clang owns the start
37column, this pass owns the interior + the ``*/`` column.
39Only **single-line** block comments are touched (opener and matching ``*/`` on
40the same physical line). Multi-line ``/** ... */`` blocks, ``//`` line
41comments, string / char / raw-string literals, decorative banners, and inline
42mid-code comments (``f(/*tag=*/x)``) are left byte-for-byte alone. The pass is
43idempotent (re-running ``--fix`` is a no-op) and clang-format-stable (a
44clang-format re-run leaves the result alone).
48 check_comment_format.py # gate the whole tree (exit 1 on drift)
49 check_comment_format.py path ... # gate only the listed files/dirs
50 check_comment_format.py --fix [path...] # rewrite in place
51 check_comment_format.py --selftest # run the built-in test battery
53Exit 0 if clean (or fixed), exit 1 on findings in check mode, exit 2 on a
54selftest failure or a whole-tree sweep that collapsed below FILE_FLOOR.
57from __future__
import annotations
60from collections.abc
import Iterable
61from pathlib
import Path
62from typing
import NamedTuple
64sys.path.insert(0, str(Path(__file__).resolve().parent))
66from lint_targets
import is_build_output_path
68REPO_ROOT = Path(__file__).resolve().parents[2]
88SCAN_ROOTS = (
"libs",
"port",
"examples",
"tools",
"apps",
"tests")
91 "apps/shared_libs/third_party/",
126_OUT_OF_SEQUENCE = (
"}",
"#")
132class _Comment(NamedTuple):
133 """One single-line block comment found in code (NORMAL) context.
135 `start`/`end` are character indices into the physical line such that
136 ``line[start:end]`` is the full ``/* ... */`` text. `opener` is the
137 normalised opener token (``/*`` / ``/**`` / ``/**<`` / ``/*!`` / ``/*!<``)
138 and `content` is the inner prose with leading/trailing whitespace stripped
139 and internal whitespace preserved. `processable` is False for banners /
140 empties that must be left untouched.
150def _classify(text: str) -> tuple[str, str, bool]:
151 """Split a ``/* ... */`` comment into (opener, content, processable).
153 `text` is the full comment including the ``/*`` and ``*/`` delimiters.
154 Returns the normalised opener token, the stripped inner content, and a
155 processability flag that is False for empty comments and decorative
156 banners (which are returned verbatim by the caller).
159 if not inner.strip():
160 return (
"/*", inner,
False)
162 return (
"/*", inner,
False)
164 if inner.startswith(
"*<"):
165 opener, rest =
"/**<", inner[2:]
166 elif inner.startswith(
"!<"):
167 opener, rest =
"/*!<", inner[2:]
168 elif inner.startswith(
"**"):
170 return (
"/*", inner,
False)
171 elif inner.startswith(
"*"):
172 opener, rest =
"/**", inner[1:]
173 elif inner.startswith(
"!"):
174 opener, rest =
"/*!", inner[1:]
176 opener, rest =
"/*", inner
178 content = rest.strip()
180 return (opener, content,
False)
182 if content.startswith(
"*")
or content.endswith(
"*"):
183 return (opener, content,
False)
184 return (opener, content,
True)
187def _scan_line(line: str, state: int, raw_delim: str) -> tuple[list[_Comment], int, str]:
188 """Tokenise one line, returning (single-line comments, new_state, raw_delim).
190 Tracks string/char/raw-string literals, ``//`` line comments, and multi-line
191 ``/* */`` blocks so that ``/*`` and ``*/`` sequences inside literals or other
192 comments are never mistaken for a comment delimiter.
194 comments: list[_Comment] = []
198 if state == _ST_BLOCK:
199 end = line.find(
"*/")
201 return (comments, _ST_BLOCK, raw_delim)
203 elif state == _ST_RAW:
204 close =
")" + raw_delim +
'"'
205 end = line.find(close)
207 return (comments, _ST_RAW, raw_delim)
212 nxt = line[i + 1]
if i + 1 < n
else ""
214 if c ==
"/" and nxt ==
"/":
216 if c ==
"/" and nxt ==
"*":
217 close = line.find(
"*/", i + 2)
219 return (comments, _ST_BLOCK, raw_delim)
220 text = line[i : close + 2]
221 opener, content, processable = _classify(text)
222 comments.append(_Comment(i, close + 2, opener, content, processable))
226 delim = _raw_string_delim(line, i)
227 if delim
is not None:
228 close =
")" + delim +
'"'
229 end = line.find(close, i + 2 + len(delim))
231 return (comments, _ST_RAW, delim)
234 i = _skip_quoted(line, i,
'"')
237 i = _skip_quoted(line, i,
"'")
241 return (comments, _ST_NORMAL,
"")
244def _raw_string_delim(line: str, quote_idx: int) -> str |
None:
245 """If ``line[quote_idx]`` opens a C++ raw string, return its ``(``-delimiter.
247 A raw string is ``R"delim(`` with an optional encoding prefix (``u8``, ``L``,
248 ``u``, ``U``) on the ``R``. Returns None when this quote is a normal string.
250 if quote_idx == 0
or line[quote_idx - 1] !=
"R":
255 while prefix_start > 0
and line[prefix_start - 1]
in "uUL8":
257 if prefix_start > 0
and (line[prefix_start - 1].isalnum()
or line[prefix_start - 1] ==
"_"):
259 open_paren = line.find(
"(", quote_idx + 1)
262 return line[quote_idx + 1 : open_paren]
265def _skip_quoted(line: str, i: int, quote: str) -> int:
266 """Return the index just past a ``quote``-delimited literal starting at `i`."""
279def _render(opener: str, content: str, pad: int) -> str:
280 """Build a normalised comment: opener, one space, content, `pad` spaces, ``*/``."""
281 return f
"{opener} {content}{' ' * pad}*/"
284def _body_len(c: _Comment) -> int:
285 """Width of the minimal (one-space) comment ``/**< content */`` for `c`."""
286 return len(c.opener) + 1 + len(c.content) + 1 + 2
291) -> tuple[list[_Comment |
None], list[str], list[int], list[tuple[int, str]]]:
292 """Classify every line's last block comment as trailing, standalone, or neither.
294 Returns four parallel results: per line, the trailing comment (``None`` when
295 the line has none), the verbatim text in front of it (code plus
296 clang-format's leading alignment), and the column that comment opens at
297 (``-1`` when there is none); plus a list of ``(index, rendered)`` pairs for
298 full-line standalone comments, which are spacing-normalised but never
301 A line whose last comment is a banner or an empty ``/**/``, or which has
302 code after the ``*/``, yields no trailing comment: neither this pass nor
303 clang-format's ``AlignTrailingComments`` treats those as alignable, so both
304 end an alignment run there.
306 trailing: list[_Comment |
None] = [
None] * len(lines)
307 prefix: list[str] = [
""] * len(lines)
308 start_col: list[int] = [-1] * len(lines)
309 standalone: list[tuple[int, str]] = []
311 state, raw_delim = _ST_NORMAL,
""
312 for i, line
in enumerate(lines):
313 comments, state, raw_delim = _scan_line(line, state, raw_delim)
317 if not last.processable
or line[last.end :].strip() !=
"":
319 before = line[: last.start]
320 if before.strip() ==
"":
321 standalone.append((i, before + _render(last.opener, last.content, 1)))
323 if not before[-1].isspace():
327 start_col[i] = len(before)
328 return trailing, prefix, start_col, standalone
331def _clang_format_off(lines: list[str]) -> set[int]:
332 """Return the indices of lines clang-format has been told to leave alone.
334 Everything from a ``clang-format off`` marker up to the matching ``on`` (or
335 end of file) is emitted verbatim, so no alignment rule can be read off it:
336 the columns there are whatever the author typed. Reporting a torn block
337 inside such a region asks for a repair clang-format would never make -- and
338 in one case for a comment whose budget was NEGATIVE, since the author had
339 switched the formatter off precisely to keep an over-long marker on its
342 off: set[int] = set()
344 for i, line
in enumerate(lines):
345 marker = _fmt_toggle(line)
356def _fmt_toggle(line: str) -> str |
None:
357 """Return ``"off"`` / ``"on"`` when `line` is a clang-format toggle comment.
359 Both delimiters count, and so does the ``clang-format off: why`` form --
360 clang-format honours a trailing explanation, and this tree uses it, so a
361 matcher demanding the bare spelling would read a live marker as ordinary
362 prose and police a region the formatter never touched.
364 stripped = line.strip()
365 if stripped.startswith(
"//"):
366 body = stripped[2:].strip()
367 elif stripped.startswith(
"/*")
and stripped.endswith(
"*/"):
368 body = stripped[2:-2].strip()
371 for word
in (
"off",
"on"):
372 if body == f
"clang-format {word}" or body.startswith(f
"clang-format {word}:"):
377class _SplitRun(NamedTuple):
378 """A block of trailing comments that cannot be aligned as a single unit.
380 `first`/`last` are the 1-based line numbers bounding the run. `cols` holds
381 the distinct 1-based columns its ``/**<`` openers start at -- more than one
382 means clang-format gave up and split the block. `over` lists the 1-based
383 lines whose minimal comment is too wide to sit at `col`, and `excess` is how
384 many characters the widest of them must lose.
389 cols: tuple[int, ...]
391 over: tuple[int, ...]
395def find_split_runs(text: str) -> list[_SplitRun]:
396 """Report every trailing-comment block the column limit tore in two.
398 A *run* is a maximal stretch of consecutive lines that each end in a
399 single-line trailing comment. A blank line, a code-only line, a standalone
400 comment, or an inline mid-code comment ends one -- and ends clang-format's
401 alignment sequence too, so a run is exactly the unit clang-format tries to
404 ``AlignTrailingComments`` aligns a run to its widest code plus one space,
405 but abandons that column and starts a fresh group when it would push the
406 longest comment past ``ColumnLimit``. The result reads as ragged: one
407 struct, two ``/**<`` columns and two ``*/`` columns. This pass cannot
408 repair it -- shortening prose is the author's call -- so it reports.
410 A run is reported when its comments open at more than one column, or when
411 the widest one, placed at the run's rightmost column, would reach the limit
412 (which is what splits the closing ``*/`` alignment on the next run of the
413 formatter). Both are the same defect measured before and after
414 clang-format has reacted to it.
416 lines = text.removesuffix(
"\n").split(
"\n")
417 trailing, _prefix, start_col, _standalone = _scan_trailing(lines)
418 for i
in _clang_format_off(lines):
421 found: list[_SplitRun] = []
422 for run
in _alignment_runs(lines, trailing):
423 cols = sorted({start_col[k]
for k
in run})
425 over = tuple(k + 1
for k
in run
if col + _body_len(trailing[k]) > _COLUMN_LIMIT)
426 if len(cols) > 1
or over:
427 widest = max(_body_len(trailing[k])
for k
in run)
432 cols=tuple(c + 1
for c
in cols),
435 excess=max(0, col + widest - _COLUMN_LIMIT),
441def _alignment_runs(lines: list[str], trailing: list[_Comment |
None]) -> list[list[int]]:
442 """Group line indices into the blocks clang-format aligns as one.
444 A block is a maximal stretch of consecutive trailing-comment lines at ONE
445 indentation, and it excludes two kinds of line that clang-format keeps out
446 of an alignment sequence regardless of how wide anything is:
448 * a line that CLOSES a scope (``}``, ``};``, ``} while (x);``) -- with
449 ``int a; /* a */`` inside a block and ``} /* done */`` after it,
450 clang-format leaves the two comments in unrelated columns;
451 * a preprocessor directive.
453 Modelling the unit matters more than it sounds. Judging a whole struct as
454 one block, when clang-format was aligning its members and its anonymous
455 union's closing ``};`` separately all along, reports a tear that is not
456 there and asks for a repair no formatter would ever make. Blocks at
457 different indentation are likewise judged apart: clang-format sometimes
458 aligns across an indent change, so treating them as one unit could only
459 manufacture findings.
461 runs: list[list[int]] = []
464 for i, line
in enumerate(lines):
465 stripped = line.lstrip()
468 or stripped.startswith(_OUT_OF_SEQUENCE)
469 or (cur
and len(line) - len(stripped) != indent)
472 if len(cur) >= _MIN_RUN:
475 if trailing[i]
is None or stripped.startswith(_OUT_OF_SEQUENCE):
478 indent = len(line) - len(stripped)
480 if len(cur) >= _MIN_RUN:
485def fix_text(text: str) -> str:
486 """Return `text` with the comment-format rules applied. Pure; idempotent.
488 Only the LAST block comment on a line is ever touched, and only when nothing
489 but whitespace follows it. That covers trailing comments (code before them)
490 and full-line standalone comments (only whitespace before). Inline mid-code
491 comments (``f(/*tag=*/x)``) are left byte-for-byte alone.
493 The text in front of a trailing comment (code + clang-format's start-column
494 alignment) is preserved verbatim -- clang-format owns the comment start
495 column. This pass only rewrites each comment from ``/**<`` to ``*/``: one
496 space after the opener, one space before the closer, and, across a run of
497 consecutive trailing comments that clang-format put in the same start
498 column, the closing ``*/`` padded so they line up under the longest (which
499 gets one space). A run is also split where end-alignment would reach the
500 column limit, so the padding never makes clang-format reflow the line.
501 Standalone full-line comments are spacing-normalised but never aligned.
503 had_trailing_nl = text.endswith(
"\n")
504 body = text[:-1]
if had_trailing_nl
else text
505 lines = body.split(
"\n")
508 trailing, prefix, start_col, standalone = _scan_trailing(lines)
509 for i, rendered
in standalone:
518 cemented = {k
for run
in find_split_runs(text)
for k
in range(run.first - 1, run.last)}
522 while i < len(lines):
523 if trailing[i]
is None:
528 out[i] = prefix[i] + _render(c.opener, c.content, 1)
532 body_hi = _body_len(trailing[i])
534 j + 1 < len(lines)
and trailing[j + 1]
is not None and start_col[j + 1] == start_col[i]
536 nbody = max(body_hi, _body_len(trailing[j + 1]))
537 if start_col[i] + nbody > _COLUMN_LIMIT:
541 for k
in range(i, j + 1):
543 pad = body_hi - _body_len(c) + 1
544 out[k] = prefix[k] + _render(c.opener, c.content, pad)
547 result =
"\n".join(out)
548 return result +
"\n" if had_trailing_nl
else result
551def _is_excluded(path: Path) -> bool:
552 return is_build_output_path(path)
or any(frag
in str(path)
for frag
in EXCLUDE_FRAGMENTS)
555def _is_source(path: Path) -> bool:
556 return path.suffix
in SOURCE_SUFFIXES
559def _rel(path: Path) -> str:
560 if path.is_relative_to(REPO_ROOT):
561 return str(path.relative_to(REPO_ROOT))
565def _enumerate_targets(arg_paths: Iterable[str]) -> list[Path]:
566 args = list(arg_paths)
571 if not path.is_absolute():
572 path = REPO_ROOT / path
574 out.extend(c
for c
in path.rglob(
"*")
if c.is_file()
and _is_source(c))
575 elif _is_source(path):
577 return [p
for p
in out
if not _is_excluded(p)]
580 for root
in SCAN_ROOTS:
581 base = REPO_ROOT / root
583 out.extend(c
for c
in base.rglob(
"*")
if c.is_file()
and _is_source(c))
584 return [p
for p
in out
if not _is_excluded(p)]
587def _first_diff_lines(old: str, new: str, limit: int = 4) -> list[tuple[int, str, str]]:
588 """Return up to `limit` (1-based lineno, old, new) tuples that differ."""
589 olines = old.split(
"\n")
590 nlines = new.split(
"\n")
592 for idx, (o, n)
in enumerate(zip(olines, nlines, strict=
False), start=1):
594 diffs.append((idx, o, n))
595 if len(diffs) >= limit:
600def main(argv: list[str]) -> int:
601 """Report, or with ``--fix`` rewrite, comment blocks whose interior is misaligned.
603 Three modes, checked in that order: ``--selftest`` runs the built-in
604 battery and ignores everything else on the line, ``--fix`` rewrites in
605 place and reports how many files changed, and the default reports offenders
606 and fails. Only ``--fix`` writes; the default mode leaves the tree alone.
608 A file that cannot be decoded as UTF-8 is skipped silently rather than
609 failed -- comment alignment is not the gate that should adjudicate
610 encoding, and check-encoding already owns that and would report it twice.
612 FILE_FLOOR applies to the whole-tree sweep ONLY, and exits 2 below it. An
613 explicit path list is how format_code.sh and the pre-commit hook drive this
614 tool, and it legitimately filters to nothing when a commit touches no C
615 source; an empty SWEEP is a broken enumeration reporting well-formed
616 comments because it read nothing.
618 Returns 0 when clean or when ``--fix`` rewrote everything it found, 1 when
619 the default mode found a file needing a rewrite, 2 when the whole-tree
620 sweep enumerated too few files to trust.
623 if "--selftest" in args:
625 do_fix =
"--fix" in args
626 paths = [a
for a
in args
if not a.startswith(
"--")]
628 targets = _enumerate_targets(paths)
629 if not paths
and len(targets) < FILE_FLOOR:
631 f
"check_comment_format.py: FATAL -- only {len(targets)} file(s) in scope, "
632 f
"floor is {FILE_FLOOR}.\n"
633 " A collapsed sweep reports well-formed comments because it read nothing.\n"
637 print(
"check_comment_format.py: no files to scan")
641 split: list[tuple[Path, _SplitRun]] = []
643 for path
in sorted(targets):
645 original = path.read_text(encoding=
"utf-8")
646 except (OSError, UnicodeDecodeError):
648 updated = fix_text(original)
649 if updated != original:
651 path.write_text(updated, encoding=
"utf-8")
655 split.extend((path, run)
for run
in find_split_runs(updated))
658 print(f
"check_comment_format.py: reformatted {fixed} file(s).")
660 _report_rewrites(bad)
663 _report_split_runs(split)
666 print(f
"check_comment_format.py: {len(targets)} file(s) scanned, comments well-formed.")
670def _report_rewrites(bad: list[Path]) ->
None:
671 """Write the first few lines this pass would rewrite in each file, to stderr."""
672 sys.stderr.write(
"check_comment_format.py: comment-format finding(s):\n")
675 original = path.read_text(encoding=
"utf-8")
676 for lineno, old, new
in _first_diff_lines(original, fix_text(original)):
678 f
" {rel}:{lineno}\n have: {old.rstrip()}\n want: {new.rstrip()}\n"
680 sys.stderr.write(
"\nRun: just quality::local::format (or check_comment_format.py --fix)\n")
683def _report_split_runs(split: list[tuple[Path, _SplitRun]]) ->
None:
684 """Write the split-run findings, and how to clear them, to stderr."""
686 f
"check_comment_format.py: {len(split)} trailing-comment block(s) too wide to align:\n"
688 for path, run
in split:
690 where = f
" {rel}:{run.first}-{run.last}"
691 if len(run.cols) > 1:
692 cols =
", ".join(str(c)
for c
in run.cols)
693 sys.stderr.write(f
"{where}: /**< opens at columns {cols} -- clang-format split it\n")
695 sys.stderr.write(f
"{where}: at column {run.col} the */ alignment splits\n")
697 lines =
", ".join(str(line)
for line
in run.over[:_MAX_NAMED_LINES])
698 more =
", ..." if len(run.over) > _MAX_NAMED_LINES
else ""
700 f
" too wide at column {run.col}: line(s) {lines}{more}"
701 f
" -- shed {run.excess} char(s)\n"
704 "\nOne block of trailing comments must align as one: a single /**< column and\n"
705 "a single */ column. clang-format abandons the alignment when the widest code\n"
706 f
"plus the longest comment would reach column {_COLUMN_LIMIT}, and this pass\n"
707 "cannot shorten prose for you. Either tighten the comments named above, or\n"
708 "move the long one to its own /** ... */ block above the line it documents.\n"
725 " a = 0x01U, /**< Sync event: VSYNC start. */\n"
726 " b = 0x08U, /**< End of Transmission packet. */\n"
731 " a = 0x01U, /**< Sync event: VSYNC start. */\n"
732 " b = 0x08U, /**< End of Transmission packet. */\n"
737_OVER_IN =
"enum {\n a = 1, /**< short. */\n b = 2, /**< longer text. */\n};\n"
738_OVER_OUT =
"enum {\n a = 1, /**< short. */\n b = 2, /**< longer text. */\n};\n"
743_SEP_IN =
" k_a = 1, /**< x.*/\n k_bb = 2, /**< yy. */\n"
744_SEP_OUT =
" k_a = 1, /**< x. */\n k_bb = 2, /**< yy. */\n"
750 " a = 1U, /**< one. */\n"
751 " b = 2U, /**< two. */\n"
753 " /**< composite. */\n"
758 " a = 1U, /**< one. */\n"
759 " b = 2U, /**< two. */\n"
761 " /**< composite. */\n"
770_CAP_IN = f
" a = 1, /**< short. */\n b = 2, /**< {_LONG_C}. */\n"
771_CAP_OUT = f
" a = 1, /**< short. */\n b = 2, /**< {_LONG_C}. */\n"
776_CEMENT_IN =
" ra8_mount_t* aa; /**< short. */\n uint32_t bb; /**< " +
"x" * 78 +
". */\n"
777_CEMENT_OUT =
" ra8_mount_t* aa; /**< short. */\n uint32_t bb; /**< " +
"x" * 78 +
". */\n"
780_MULTI =
"/**\n * @brief foo.*/bar\n * body\n */\nint x;\n"
782_SELFTEST_CASES: tuple[tuple[str, str, str], ...] = (
784 (
"space after /*",
"int x; /*hi */\n",
"int x; /* hi */\n"),
786 (
"space before */",
"int x; /* hi*/\n",
"int x; /* hi */\n"),
789 "bool e; /**< DSISETR.EOTPEN.*/\n",
790 "bool e; /**< DSISETR.EOTPEN. */\n",
793 (
"space before comment",
"int x;/* hi */\n",
"int x; /* hi */\n"),
794 (
"align run to longest", _RUN_IN, _RUN_OUT),
795 (
"tighten over-padded run", _OVER_IN, _OVER_OUT),
796 (
"leading preserved, columns separate", _SEP_IN, _SEP_OUT),
797 (
"code line breaks run", _BREAK_IN, _BREAK_OUT),
798 (
"column-limit splits run", _CAP_IN, _CAP_OUT),
799 (
"a reported block is left un-padded", _CEMENT_IN, _CEMENT_OUT),
801 (
"string with /* */",
'const char* s = "/*x*/";\n',
'const char* s = "/*x*/";\n'),
802 (
"string with */",
'puts("a*/b");\n',
'puts("a*/b");\n'),
804 (
"line comment left alone",
"int x; // a*/b\n",
"int x; // a*/b\n"),
805 (
"multiline block untouched", _MULTI, _MULTI),
807 (
"banner untouched",
"/******** section ********/\n",
"/******** section ********/\n"),
808 (
"empty comment untouched",
"x; /**/\n",
"x; /**/\n"),
810 (
"raw string untouched",
'auto s = R"(a*/b/*c)";\n',
'auto s = R"(a*/b/*c)";\n'),
813 "internal spacing kept",
814 "x = 1; /**< VBTBPSR Ch 12.2 p 509.*/\n",
815 "x = 1; /**< VBTBPSR Ch 12.2 p 509. */\n",
820 (
"inline arg-label spaced",
"f(a, /* tag= */ b);\n",
"f(a, /* tag= */ b);\n"),
821 (
"inline arg-label tight",
"f(a, /*tag=*/b);\n",
"f(a, /*tag=*/b);\n"),
822 (
"inline then trailing",
"f(/*a*/x); /*hi*/\n",
"f(/*a*/x); /* hi */\n"),
830 f
"struct s {{\n ra8_mount_t* aa; /**< short. */\n uint32_t bb; /**< {'x' * 78}. */\n}};\n"
836_SPLIT_COLS_ONLY =
" k_a = 1, /**< x. */\n k_bb = 2, /**< yy. */\n"
839_SPLIT_ONE_COL = f
" a = 1, /**< short. */\n b = 2, /**< {'x' * 88}. */\n"
842_QUIET_RUN =
"struct s {\n uint32_t a; /**< one. */\n uint32_t b; /**< two. */\n};\n"
847 " const paint_t* a; /**< one. */\n"
848 " void (*cb)(int x, int y);\n"
849 " uint32_t b; /**< two. */\n"
853_QUIET_BLANK =
"struct s {\n const paint_t* a; /**< one. */\n\n uint32_t b; /**< two. */\n};\n"
855_QUIET_SINGLE = f
" uint32_t a; /**< {'x' * 60}. */\n"
858_QUIET_FMT_OFF =
"// clang-format off\n" + _SPLIT_TWO_COLS +
"// clang-format on\n"
860_QUIET_FMT_OFF_WHY = (
861 "// clang-format off: the marker must stay on the call line.\n" + _SPLIT_TWO_COLS
865_SPLIT_AFTER_ON =
"// clang-format off\nint x; /* a */\n// clang-format on\n" + _SPLIT_TWO_COLS
871_QUIET_BRACE =
" } /* close */\n return; /* ret */\n"
874_QUIET_PREPROC =
"int aaaaaa; /* one */\n#endif /* end */\n"
877_QUIET_INDENT =
" int a; /* one */\n int bb; /* two */\n"
879_SPLIT_CASES: tuple[tuple[str, str, int], ...] = (
880 (
"two /**< columns reported", _SPLIT_TWO_COLS, 1),
881 (
"two columns alone are enough", _SPLIT_COLS_ONLY, 1),
882 (
"one column, */ alignment splits", _SPLIT_ONE_COL, 1),
883 (
"aligned block stays quiet", _QUIET_RUN, 0),
884 (
"code line ends the run", _QUIET_BREAK, 0),
885 (
"blank line ends the run", _QUIET_BLANK, 0),
886 (
"single comment stays quiet", _QUIET_SINGLE, 0),
887 (
"clang-format off exempts the region", _QUIET_FMT_OFF, 0),
888 (
"the off: why spelling exempts too", _QUIET_FMT_OFF_WHY, 0),
889 (
"a scope-closing brace is out of the run", _QUIET_BRACE, 0),
890 (
"a preprocessor directive is out of the run", _QUIET_PREPROC, 0),
891 (
"an indent change is judged apart", _QUIET_INDENT, 0),
892 (
"the exemption ends at clang-format on", _SPLIT_AFTER_ON, 1),
896def _check_split_case(name: str, src: str, want: int) -> int:
897 """Run one detector case; return the number of failures it produced (0 or 1).
899 The detector is run on this pass's own output, which is how `main` drives
900 it: a finding that only survives on unformatted input would be noise.
902 got = len(find_split_runs(fix_text(src)))
904 sys.stderr.write(f
"[FAIL] {name}: want {want} finding(s), got {got}\n")
909def _check_case(name: str, src: str, want: str) -> int:
910 """Run one rewrite case; return the number of failures it produced (0 or 1).
912 Idempotency is asserted alongside correctness because this pass runs in a
913 formatter loop: a rule that keeps changing its own output would churn every
918 sys.stderr.write(f
"[FAIL] {name}\n want: {want!r}\n got: {got!r}\n")
920 if fix_text(got) != got:
921 sys.stderr.write(f
"[FAIL] {name}: not idempotent\n")
926def _selftest() -> int:
927 failures = sum(_check_case(*case)
for case
in _SELFTEST_CASES)
928 failures += sum(_check_split_case(*case)
for case
in _SPLIT_CASES)
929 total = len(_SELFTEST_CASES) + len(_SPLIT_CASES)
931 sys.stderr.write(f
"check_comment_format.py: selftest FAILED ({failures} case(s)).\n")
933 print(f
"check_comment_format.py: selftest passed ({total} cases).")
937if __name__ ==
"__main__":
938 sys.exit(
main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.