Skip to content

Commit b0d403d

Browse files
sbryngelsonclaude
andcommitted
Auto-fix @file briefs at build time to match module declarations
Add docs/fix_file_briefs.py that reads the actual module/program name from each Fortran source file and ensures the @file @brief matches, using @ref for mixed-case identifiers. Runs before Doxygen via CMake so new files, renames, and case issues are caught automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d5471e4 commit b0d403d

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,22 @@ if (MFC_DOCUMENTATION)
822822
add_dependencies(simulation_doxygen gen_api_landing)
823823
add_dependencies(post_process_doxygen gen_api_landing)
824824

825+
# Fix @file/@brief headers to match actual module/program declarations.
826+
# Handles mixed-case Fortran names and catches stale module renames.
827+
add_custom_command(
828+
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/fix-file-briefs.stamp"
829+
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/docs/fix_file_briefs.py"
830+
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/docs/fix_file_briefs.py"
831+
"${CMAKE_CURRENT_SOURCE_DIR}"
832+
COMMAND "${CMAKE_COMMAND}" -E touch "${CMAKE_CURRENT_BINARY_DIR}/fix-file-briefs.stamp"
833+
COMMENT "Fixing @file brief headers"
834+
VERBATIM
835+
)
836+
add_custom_target(fix_file_briefs DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/fix-file-briefs.stamp")
837+
add_dependencies(pre_process_doxygen fix_file_briefs)
838+
add_dependencies(simulation_doxygen fix_file_briefs)
839+
add_dependencies(post_process_doxygen fix_file_briefs)
840+
825841
# Inject per-page last-updated dates into documentation markdown files.
826842
# Runs after auto-generated .md files exist, before Doxygen processes them.
827843
# Uses a stamp file so it only runs once per build.

docs/fix_file_briefs.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env python3
2+
"""Ensure @file/@brief headers match actual module/program declarations.
3+
4+
Usage: python3 fix_file_briefs.py [source_dir]
5+
source_dir defaults to current directory.
6+
7+
For each .fpp/.f90 in src/{pre_process,simulation,post_process,common}:
8+
1. Parses the first `module <name>` or `program <name>` declaration.
9+
2. If no @file directive exists in the first 15 lines, prepends a header.
10+
3. If a "Contains module/program ..." @brief exists, rewrites the name
11+
to match the source, using @ref for mixed-case Fortran identifiers
12+
(Doxygen lowercases Fortran namespaces).
13+
"""
14+
15+
import re
16+
import sys
17+
from pathlib import Path
18+
19+
src_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
20+
21+
DIRS = [
22+
src_dir / "src" / "pre_process",
23+
src_dir / "src" / "simulation",
24+
src_dir / "src" / "post_process",
25+
src_dir / "src" / "common",
26+
]
27+
28+
# First `module X` or `program X` that isn't `end module/program`.
29+
DECL_RE = re.compile(
30+
r"^\s*(module|program)\s+(\w+)\s*$", re.MULTILINE | re.IGNORECASE
31+
)
32+
33+
# Any "Contains module/program <name>" in a Doxygen comment line.
34+
# Matches both `!! @brief Contains ...` and `!> @brief Contains ...`.
35+
BRIEF_CONTAINS_RE = re.compile(
36+
r"^(!!|!>)\s*@brief\s+(Contains (?:module|program) )(.*)",
37+
re.MULTILINE,
38+
)
39+
40+
41+
def find_entity(text: str) -> tuple[str, str] | None:
42+
"""Return (kind, name) of the first module/program declaration."""
43+
for m in DECL_RE.finditer(text):
44+
line_start = text.rfind("\n", 0, m.start()) + 1
45+
line = text[line_start : m.end()].strip()
46+
if line.lower().startswith("end"):
47+
continue
48+
return m.group(1).lower(), m.group(2)
49+
return None
50+
51+
52+
def make_ref(name: str) -> str:
53+
"""Return @ref for mixed-case names, plain name for lowercase."""
54+
lower = name.lower()
55+
return f'@ref {lower} "{name}"' if lower != name else name
56+
57+
58+
def has_file_directive(text: str) -> bool:
59+
"""Check if @file appears in the first 15 lines."""
60+
head = "\n".join(text.splitlines()[:15])
61+
return bool(re.search(r"@file", head))
62+
63+
64+
fixed = 0
65+
for d in DIRS:
66+
if not d.exists():
67+
continue
68+
for f in sorted(list(d.glob("*.fpp")) + list(d.glob("*.f90"))):
69+
text = f.read_text()
70+
entity = find_entity(text)
71+
if entity is None:
72+
continue
73+
kind, name = entity
74+
ref = make_ref(name)
75+
76+
if not has_file_directive(text):
77+
# No @file at all — prepend a complete header.
78+
header = f"!>\n!! @file\n!! @brief Contains {kind} {ref}\n\n"
79+
text = header + text
80+
f.write_text(text)
81+
fixed += 1
82+
print(f"Added {f.relative_to(src_dir)}")
83+
continue
84+
85+
# Has @file — check if there's a "Contains module/program" brief to fix.
86+
m = BRIEF_CONTAINS_RE.search(text)
87+
if m is None:
88+
continue # Has @file but no "Contains ..." brief — leave it alone
89+
90+
current_name = m.group(3).strip()
91+
if current_name == ref:
92+
continue # Already correct
93+
94+
# Replace the name portion of the brief line.
95+
new_line = f"{m.group(1)} @brief {m.group(2)}{ref}"
96+
new_text = text[: m.start()] + new_line + text[m.end() :]
97+
98+
if new_text != text:
99+
f.write_text(new_text)
100+
fixed += 1
101+
print(f"Fixed {f.relative_to(src_dir)}: {current_name} -> {ref}")
102+
103+
print(f"Done — {fixed} file(s) updated.")

0 commit comments

Comments
 (0)