From 045aee0d11951ca50cea0a5e2c6c5efbbf78a690 Mon Sep 17 00:00:00 2001 From: Martin Anderseck Date: Thu, 21 May 2026 15:48:46 +0200 Subject: [PATCH] pre-commit: Add Doxygen comment format check This adds a basic check for the correct format of Doxygen comments. Since there were comments that started with `//!` and continued in the next line with `//`, but Doxygen only used the first line, this ensures that there aren't any such comments anymore. Refer to the coding guidelines to see the correct format for documentation in the code. --- .pre-commit-config.yaml | 6 ++ tools/check_doxygen_format.py | 135 ++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 tools/check_doxygen_format.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ce56407e..7cc6b1946 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,3 +19,9 @@ repos: entry: ni-python-styleguide lint # TODO: re-enable linting until we have the fixed all files at once exclude: ^mpm + - id: check-doxygen-format + name: check_doxygen_format + description: Check for broken Doxygen comments in C++ files + language: python + types: [c++] + entry: python tools/check_doxygen_format.py diff --git a/tools/check_doxygen_format.py b/tools/check_doxygen_format.py new file mode 100644 index 000000000..ecec4213f --- /dev/null +++ b/tools/check_doxygen_format.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Find likely broken Doxygen single-line comments. + +Pattern flagged: + //! First line + // continuation line + +I.e., a line starting with `//!` followed by one or more immediately following +lines that start with plain `//` (exactly two slashes, not `//!` or `///`). + +Only `.hpp` and `.cpp` files are scanned. +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +DOC_START_PREFIX = "//!" +PLAIN_COMMENT_PREFIX = "//" + + +def parse_args() -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description=( + "Scan .hpp/.cpp files for likely broken Doxygen comments where " + "a //! line is followed by plain // continuation lines." + ) + ) + parser.add_argument( + "files", + nargs="*", + type=Path, + help=("Optional explicit file list to scan. Non-.cpp/.hpp entries are ignored."), + ) + return parser.parse_args() + + +def is_plain_comment_line(line: str) -> bool: + """Check if a line is a plain comment line (starts with // but not //! or ///).""" + stripped = line.lstrip() + if not stripped.startswith(PLAIN_COMMENT_PREFIX): + return False + if stripped.startswith("//!") or stripped.startswith("///"): + return False + return True + + +def find_matches_in_file(path: Path) -> list[int]: + """Find likely broken Doxygen comments in a file.""" + try: + lines = path.read_text(encoding="utf-8", errors="replace").splitlines() + except OSError: + return [] + + matches: list[int] = [] + i = 0 + while i < len(lines): + stripped = lines[i].lstrip() + if stripped.startswith(DOC_START_PREFIX): + j = i + 1 + saw_plain_comment = False + while j < len(lines) and is_plain_comment_line(lines[j]): + saw_plain_comment = True + j += 1 + if saw_plain_comment: + matches.append(i + 1) # 1-based line number of //! start + i = j + continue + i += 1 + + return matches + + +def iter_selected_cpp_headers(root: Path, files: list[Path]): + """Iterates over the given file paths, yielding only those that are .cpp or .hpp files.""" + seen: set[Path] = set() + for p in files: + candidate = p if p.is_absolute() else (root / p) + try: + resolved = candidate.resolve() + except OSError: + continue + + if resolved in seen: + continue + seen.add(resolved) + + if not resolved.exists() or not resolved.is_file(): + continue + yield resolved + + +def main() -> int: + """Main entry point.""" + args = parse_args() + root = Path(__file__).resolve().parents[1] + + total_files = 0 + total_hits = 0 + + if args.files: + file_iter = iter_selected_cpp_headers(root, args.files) + else: + print("No files to check.") + return 0 + + for file_path in file_iter: + total_files += 1 + hits = find_matches_in_file(file_path) + if not hits: + continue + try: + rel = file_path.relative_to(root) + except ValueError: + rel = file_path + for line_no in hits: + print(f"{rel}:{line_no}") + total_hits += 1 + + if total_hits: + print( + f"\nFound {total_hits} likely broken Doxygen comment block(s) in " + f"{total_files} scanned file(s).", + file=sys.stderr, + ) + return 1 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())