mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-10-13 19:20:54 +02:00
Audit-clean C standard across all 5 cb challenges: fix 3 functions over 8 exec lines; rebuild bins; update CB-0x03 and CB-0x05 guides; verify all guides against new bins
This commit is contained in:
1 parent
4ade66738f
commit
23b9f628ef
51 files changed
+3744
-7904
No files matched your search
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: embedded-c-standard
|
||||
description: Use ONLY when writing, editing, or reviewing C firmware in a picokit project (firmware/src, firmware/include, firmware/tests). Enforces the strict C standard: no function over 8 executable lines, no blank lines inside a function body, a Doxygen block with @brief/@param/@return on every function, a fixed module layout and naming scheme, and a ban on em/en dashes. Run the bundled audit_c_standard.py before every commit.
|
||||
---
|
||||
|
||||
# Embedded C standard (picokit)
|
||||
|
||||
This is non-negotiable and identical in every project in the series. There is
|
||||
no variation between projects. If a rule is inconvenient, refactor; do not
|
||||
exempt.
|
||||
|
||||
## Rules
|
||||
|
||||
1. No function longer than 8 executable lines. The audit counts every
|
||||
non-blank, non-comment, non-brace line in the body, including continuation
|
||||
lines of a wrapped statement. Split the work into a `static` helper.
|
||||
2. No blank lines inside a function body. Separate logical steps with a
|
||||
comment, never with a blank line.
|
||||
3. Documentation lives in the header. Every function declaration in a `.h`
|
||||
carries a Doxygen block with `@brief`, a `@param` for each parameter
|
||||
(`@param void No parameters.` when there are none), and an `@return`. A
|
||||
`static` function, which has no declaration, carries its Doxygen block in
|
||||
the `.c`. Never duplicate the block in the `.c` for an exported function.
|
||||
4. No U+2013 (en dash) or U+2014 (em dash) anywhere. Use the hyphen-minus `-`.
|
||||
5. Every source file opens with the MIT header block: `Copyright (c) 2026 Kevin
|
||||
Thomas`, `Author`, `Email`, `GitHub`, `File`, `Desc`, `Created`.
|
||||
6. Every header uses an include guard `#ifndef <MODULE>_H` / `#define
|
||||
<MODULE>_H` / `#endif // <MODULE>_H`.
|
||||
7. Naming: files and modules `snake_case`; functions `module_action`; macros
|
||||
`UPPER_SNAKE`; types `name_t`; file-scope statics `g_name`.
|
||||
8. One responsibility per file: `src/<module>.c` with `include/<module>.h`.
|
||||
All hardware access goes through a HAL seam so the host tests can substitute
|
||||
mocks.
|
||||
9. 79-column lines, four-space indent, no tabs.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
firmware/
|
||||
src/ <module>.c
|
||||
include/ <module>.h
|
||||
tests/ host unit tests, mock/, harness/
|
||||
CMakeLists.txt
|
||||
scripts/ run_tests.py check_coverage.py build.sh
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
Run the bundled audit from the repository root. Exit 0 is clean; any output is
|
||||
a failure and must be fixed before the change is complete.
|
||||
|
||||
```bash
|
||||
python3 .opencode/skill/embedded-c-standard/audit_c_standard.py
|
||||
```
|
||||
@@ -0,0 +1,989 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit owned C files against the strict embedded C standard.
|
||||
|
||||
Checks the no-blank-line-in-function-body rule, the eight-executable-line
|
||||
function limit, the em/en dash ban, and the documentation placement rules:
|
||||
a C file documents its static helpers but never its exported definitions,
|
||||
while a header documents every prototype. Run from the repository root;
|
||||
exit zero means the tree is clean.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
EXCLUDED_PARTS = (
|
||||
".opencode",
|
||||
"build",
|
||||
"build/",
|
||||
"generated/",
|
||||
".venv/",
|
||||
"node_modules/",
|
||||
"packet_artifact.h",
|
||||
"third_party/",
|
||||
"test/unity",
|
||||
"unity.c",
|
||||
"unity_internals.h",
|
||||
"ghidra/",
|
||||
)
|
||||
|
||||
EN_DASH = "\u2013"
|
||||
EM_DASH = "\u2014"
|
||||
MAX_EXEC_LINES = 8
|
||||
|
||||
|
||||
def _owned_files() -> list[Path]:
|
||||
"""
|
||||
Find owned C and header files.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[pathlib.Path]
|
||||
Owned non-generated C and header paths.
|
||||
"""
|
||||
paths = Path(".").glob("**/*")
|
||||
return sorted(path for path in paths if _is_owned(path))
|
||||
|
||||
|
||||
def _is_owned(path: Path) -> bool:
|
||||
"""
|
||||
Determine whether a path is in the audit scope.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
Candidate source path.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the path is an owned C or header file.
|
||||
"""
|
||||
name = str(path)
|
||||
if any(part in name for part in EXCLUDED_PARTS):
|
||||
return False
|
||||
return path.suffix in {".c", ".h"}
|
||||
|
||||
|
||||
def _next_is(line: str, index: int, mark: str) -> bool:
|
||||
"""
|
||||
Test the character following an index against a marker.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Current character index.
|
||||
mark : str
|
||||
Single-character marker to compare.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the next character equals the marker.
|
||||
"""
|
||||
return line[index + 1:index + 2] == mark
|
||||
|
||||
|
||||
def _is_comment_open(line: str, index: int) -> bool:
|
||||
"""
|
||||
Test whether a slash begins a line or block comment.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Current character index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when a comment opens at the index.
|
||||
"""
|
||||
return line[index] == "/" and line[index + 1:index + 2] in ("*", "/")
|
||||
|
||||
|
||||
def _comment_open(line: str, index: int) -> tuple[int, str]:
|
||||
"""
|
||||
Advance past a comment opening at the index.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Index of the opening slash.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int, str]
|
||||
Next index and the resulting scanner mode.
|
||||
"""
|
||||
if _next_is(line, index, "/"):
|
||||
return len(line), "normal"
|
||||
return index + 2, "block"
|
||||
|
||||
|
||||
def _literal_mode(char: str) -> str:
|
||||
"""
|
||||
Select the scanner mode for a quote character.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
char : str
|
||||
Quote character.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Scanner mode name for the literal.
|
||||
"""
|
||||
return "string" if char == '"' else "char"
|
||||
|
||||
|
||||
def _literal_close(mode: str) -> str:
|
||||
"""
|
||||
Return the closing quote for a literal mode.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mode : str
|
||||
Scanner mode name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Matching closing quote character.
|
||||
"""
|
||||
return '"' if mode == "string" else "'"
|
||||
|
||||
|
||||
def _on_normal(out: list, line: str, index: int) -> tuple[int, str]:
|
||||
"""
|
||||
Consume one character of ordinary code.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
out : list
|
||||
Mutable code accumulator.
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Current character index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int, str]
|
||||
Next index and the resulting scanner mode.
|
||||
"""
|
||||
char = line[index]
|
||||
if _is_comment_open(line, index):
|
||||
return _comment_open(line, index)
|
||||
if char in "\"'":
|
||||
return index + 1, _literal_mode(char)
|
||||
out.append(char)
|
||||
return index + 1, "normal"
|
||||
|
||||
|
||||
def _on_block(out: list, line: str, index: int) -> tuple[int, str]:
|
||||
"""
|
||||
Consume one character inside a block comment.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
out : list
|
||||
Mutable code accumulator.
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Current character index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int, str]
|
||||
Next index and the resulting scanner mode.
|
||||
"""
|
||||
if line[index] == "*" and _next_is(line, index, "/"):
|
||||
return index + 2, "normal"
|
||||
return index + 1, "block"
|
||||
|
||||
|
||||
def _on_literal(out: list, line: str, index: int,
|
||||
mode: str) -> tuple[int, str]:
|
||||
"""
|
||||
Consume one character inside a string or character literal.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
out : list
|
||||
Mutable code accumulator.
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Current character index.
|
||||
mode : str
|
||||
Current literal scanner mode.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int, str]
|
||||
Next index and the resulting scanner mode.
|
||||
"""
|
||||
char = line[index]
|
||||
if char == "\\":
|
||||
return index + 2, mode
|
||||
if char == _literal_close(mode):
|
||||
return index + 1, "normal"
|
||||
return index + 1, mode
|
||||
|
||||
|
||||
def _advance(out: list, line: str, index: int, mode: str) -> tuple[int, str]:
|
||||
"""
|
||||
Dispatch one character to its scanner handler.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
out : list
|
||||
Mutable code accumulator.
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Current character index.
|
||||
mode : str
|
||||
Current scanner mode.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int, str]
|
||||
Next index and the resulting scanner mode.
|
||||
"""
|
||||
if mode == "normal":
|
||||
return _on_normal(out, line, index)
|
||||
if mode == "block":
|
||||
return _on_block(out, line, index)
|
||||
return _on_literal(out, line, index, mode)
|
||||
|
||||
|
||||
def _strip_line(line: str, in_block: bool) -> tuple[str, bool]:
|
||||
"""
|
||||
Remove comments and literals from one source line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Source line.
|
||||
in_block : bool
|
||||
Whether a block comment is already open.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[str, bool]
|
||||
Code-only text and the updated block-comment state.
|
||||
"""
|
||||
out = []
|
||||
index = 0
|
||||
mode = "block" if in_block else "normal"
|
||||
while index < len(line):
|
||||
index, mode = _advance(out, line, index, mode)
|
||||
return "".join(out), mode == "block"
|
||||
|
||||
|
||||
def _func_name(code: str) -> str:
|
||||
"""
|
||||
Extract the function identifier from a signature head.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code : str
|
||||
Comment-free signature line.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Trailing identifier before the opening parenthesis.
|
||||
"""
|
||||
head = code.split("(")[0].strip()
|
||||
return head.split()[-1] if head else ""
|
||||
|
||||
|
||||
def _scan_state(path: Path) -> dict:
|
||||
"""
|
||||
Build the mutable scanner state for one file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Scanner accumulation state.
|
||||
"""
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
state = {"lines": lines, "records": [], "protos": []}
|
||||
state.update(depth=0, in_block=False, cont=False, start=None)
|
||||
state.update(name="", static=False, sig=0, opened=0, body=[])
|
||||
return state
|
||||
|
||||
|
||||
def _scan_line(state: dict, line: str, number: int) -> None:
|
||||
"""
|
||||
Apply one source line to the scanner state.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
line : str
|
||||
Raw source line.
|
||||
number : int
|
||||
One-based line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
code, state["in_block"] = _strip_line(line, state["in_block"])
|
||||
_scan_open(state, code, number)
|
||||
active = state["start"] is not None
|
||||
if active and number not in (state["start"], state["opened"]):
|
||||
_collect(code, number, state["body"])
|
||||
state["depth"] += code.count("{") - code.count("}")
|
||||
_scan_close(state, number)
|
||||
state["cont"] = line.rstrip().endswith("\\")
|
||||
|
||||
|
||||
def _scan_open(state: dict, code: str, number: int) -> None:
|
||||
"""
|
||||
Open a signature, body, or prototype when one begins.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
code : str
|
||||
Comment-free source line.
|
||||
number : int
|
||||
One-based line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if state["cont"] or state["start"] is not None:
|
||||
return
|
||||
if state["depth"] != 0:
|
||||
return
|
||||
_pending(state, code, number)
|
||||
|
||||
|
||||
def _pending(state: dict, code: str, number: int) -> None:
|
||||
"""
|
||||
Resolve a file-scope signature fragment into a record.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
code : str
|
||||
Comment-free source line.
|
||||
number : int
|
||||
One-based line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if state["sig"] == 0 and _proto_line(code):
|
||||
_add_proto(state, code, number)
|
||||
elif state["sig"] == 0 and _sig_start(code):
|
||||
_start_sig(state, code, number)
|
||||
elif state["sig"] and "{" in code:
|
||||
_start_body(state, number)
|
||||
elif state["sig"] and ";" in code:
|
||||
_finish_proto(state)
|
||||
|
||||
|
||||
def _add_proto(state: dict, code: str, number: int) -> None:
|
||||
"""
|
||||
Record a single-line function prototype.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
code : str
|
||||
Comment-free source line.
|
||||
number : int
|
||||
One-based line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
state["protos"].append({"name": _func_name(code), "start": number})
|
||||
|
||||
|
||||
def _start_sig(state: dict, code: str, number: int) -> None:
|
||||
"""
|
||||
Begin a multi-line signature or same-line definition.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
code : str
|
||||
Comment-free source line.
|
||||
number : int
|
||||
One-based line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
state["sig"] = number
|
||||
state["name"] = _func_name(code)
|
||||
state["static"] = code.lstrip().startswith("static")
|
||||
if "{" in code:
|
||||
_start_body(state, number)
|
||||
|
||||
|
||||
def _finish_proto(state: dict) -> None:
|
||||
"""
|
||||
Record a wrapped prototype at its terminating semicolon.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
state["protos"].append({"name": state["name"], "start": state["sig"]})
|
||||
state["sig"] = 0
|
||||
state["name"] = ""
|
||||
state["static"] = False
|
||||
|
||||
|
||||
def _start_body(state: dict, number: int) -> None:
|
||||
"""
|
||||
Open a new function body at the given line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
number : int
|
||||
One-based opening line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
state["start"] = state["sig"]
|
||||
state["opened"] = number
|
||||
state["body"] = []
|
||||
state["sig"] = 0
|
||||
|
||||
|
||||
def _scan_close(state: dict, number: int) -> None:
|
||||
"""
|
||||
Close the current function body when its depth returns to zero.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
number : int
|
||||
One-based line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if state["start"] is None or state["depth"] > 0:
|
||||
return
|
||||
if number <= state["start"]:
|
||||
return
|
||||
state["records"].append(_record(state, number))
|
||||
_reset(state)
|
||||
|
||||
|
||||
def _reset(state: dict) -> None:
|
||||
"""
|
||||
Clear the active-function fields of the scanner state.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
state["start"] = None
|
||||
state["name"] = ""
|
||||
state["sig"] = 0
|
||||
state["opened"] = 0
|
||||
state["static"] = False
|
||||
|
||||
|
||||
def _sig_start(code: str) -> bool:
|
||||
"""
|
||||
Detect the first line of a function definition signature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code : str
|
||||
Comment-free source line.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the line begins a function definition signature.
|
||||
"""
|
||||
text = code.strip()
|
||||
if not text or text.startswith("#") or text.startswith("}"):
|
||||
return False
|
||||
if "(" not in text or text.endswith(";"):
|
||||
return False
|
||||
return "=" not in text.split("(")[0]
|
||||
|
||||
|
||||
def _proto_line(code: str) -> bool:
|
||||
"""
|
||||
Detect a complete single-line function prototype.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code : str
|
||||
Comment-free source line.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the line is a function prototype.
|
||||
"""
|
||||
text = code.strip()
|
||||
if not text or text.startswith("#") or "(" not in text:
|
||||
return False
|
||||
if not text.endswith(";") or "=" in text.split("(")[0]:
|
||||
return False
|
||||
return text.split()[0] not in {"typedef", "return"}
|
||||
|
||||
|
||||
def _collect(code: str, number: int, body: list) -> None:
|
||||
"""
|
||||
Append one executable or declaration line to the body list.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code : str
|
||||
Comment-free source line.
|
||||
number : int
|
||||
One-based line number.
|
||||
body : list
|
||||
Mutable body accumulator.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
text = code.strip()
|
||||
if not text or text in {"{", "}"} or text.startswith("}"):
|
||||
return
|
||||
body.append((number, text))
|
||||
|
||||
|
||||
def _record(state: dict, end: int) -> dict:
|
||||
"""
|
||||
Build a function record from the scanner state.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
end : int
|
||||
Closing brace line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Function record.
|
||||
"""
|
||||
return {"name": state["name"], "start": state["start"], "end": end,
|
||||
"count": len(state["body"]), "static": state["static"]}
|
||||
|
||||
|
||||
def _scan(path: Path) -> dict:
|
||||
"""
|
||||
Scan one file into records and prototypes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Scanner state with records and prototypes.
|
||||
"""
|
||||
state = _scan_state(path)
|
||||
for number, line in enumerate(state["lines"], 1):
|
||||
_scan_line(state, line, number)
|
||||
return state
|
||||
|
||||
|
||||
def _blank_lines(path: Path, records: list) -> list[int]:
|
||||
"""
|
||||
Find blank lines inside function bodies.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
records : list
|
||||
Function records for the file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[int]
|
||||
One-based blank-line numbers.
|
||||
"""
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
spans = [(r["start"], r["end"]) for r in records]
|
||||
return [number for number, line in enumerate(lines, 1)
|
||||
if not line.strip() and _inside(number, spans)]
|
||||
|
||||
|
||||
def _inside(number: int, spans: list) -> bool:
|
||||
"""
|
||||
Check whether a line lies inside a function span.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
number : int
|
||||
One-based source line number.
|
||||
spans : list
|
||||
Function start and end line pairs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the line is inside a function body.
|
||||
"""
|
||||
return any(start < number < end for start, end in spans)
|
||||
|
||||
|
||||
def _block_end(lines: list, start: int) -> int | None:
|
||||
"""
|
||||
Locate the closing line of the doc block above a signature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lines : list
|
||||
Source lines.
|
||||
start : int
|
||||
One-based signature line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int or None
|
||||
Zero-based closing `*/` index, or None when absent.
|
||||
"""
|
||||
index = start - 2
|
||||
while index >= 0 and not lines[index].strip():
|
||||
index -= 1
|
||||
if index < 0 or "*/" not in lines[index]:
|
||||
return None
|
||||
return index
|
||||
|
||||
|
||||
def _block_lines(lines: list, index: int) -> list:
|
||||
"""
|
||||
Collect the Doxygen block lines ending at an index.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lines : list
|
||||
Source lines.
|
||||
index : int
|
||||
Zero-based closing `*/` index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
Block lines from opening `/**` through closing `*/`.
|
||||
"""
|
||||
block = []
|
||||
while index >= 0:
|
||||
block.append(lines[index])
|
||||
if "/*" in lines[index]:
|
||||
break
|
||||
index -= 1
|
||||
block.reverse()
|
||||
return block
|
||||
|
||||
|
||||
def _is_doxygen(block: list) -> bool:
|
||||
"""
|
||||
Test whether a comment block is a Doxygen block with a brief.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
block : list
|
||||
Comment block lines from opener through closer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the block opens with `/**` and carries `@brief`.
|
||||
"""
|
||||
opener = block[0] if block else ""
|
||||
return "/**" in opener and "@brief" in "\n".join(block)
|
||||
|
||||
|
||||
def _doc_block(lines: list, start: int) -> list | None:
|
||||
"""
|
||||
Return the Doxygen block immediately above a signature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lines : list
|
||||
Source lines.
|
||||
start : int
|
||||
One-based signature line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list or None
|
||||
Block lines, or None when no Doxygen block precedes the signature.
|
||||
"""
|
||||
index = _block_end(lines, start)
|
||||
if index is None:
|
||||
return None
|
||||
block = _block_lines(lines, index)
|
||||
if not _is_doxygen(block):
|
||||
return None
|
||||
return block
|
||||
|
||||
|
||||
def _full_doc(block: list | None) -> bool:
|
||||
"""
|
||||
Check a static-function block for all required tags.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
block : list or None
|
||||
Doxygen block lines.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when @brief, @param, and @return are all present.
|
||||
"""
|
||||
if block is None:
|
||||
return False
|
||||
text = "\n".join(block)
|
||||
return all(tag in text for tag in ("@brief", "@param", "@return"))
|
||||
|
||||
|
||||
def _brief_doc(block: list | None) -> bool:
|
||||
"""
|
||||
Check a prototype block for a brief tag.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
block : list or None
|
||||
Doxygen block lines.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when a @brief tag is present.
|
||||
"""
|
||||
return block is not None and "@brief" in "\n".join(block)
|
||||
|
||||
|
||||
def _c_doc_errors(path: Path, lines: list, records: list) -> list[str]:
|
||||
"""
|
||||
Find documentation-placement errors in a C source file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C source path.
|
||||
lines : list
|
||||
Source lines.
|
||||
records : list
|
||||
Function records for the file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
Human-readable documentation errors.
|
||||
"""
|
||||
errors = []
|
||||
for record in records:
|
||||
block = _doc_block(lines, record["start"])
|
||||
if record["static"] and not _full_doc(block):
|
||||
errors.append(f"{path}: {record['name']} static needs full doc")
|
||||
if not record["static"] and block is not None:
|
||||
errors.append(f"{path}: {record['name']} exported has doc block")
|
||||
return errors
|
||||
|
||||
|
||||
def _h_doc_errors(path: Path, lines: list, protos: list) -> list[str]:
|
||||
"""
|
||||
Find prototypes missing a @brief block in a header.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
Header path.
|
||||
lines : list
|
||||
Source lines.
|
||||
protos : list
|
||||
Prototype records for the file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
Human-readable documentation errors.
|
||||
"""
|
||||
errors = []
|
||||
for proto in protos:
|
||||
if not _brief_doc(_doc_block(lines, proto["start"])):
|
||||
errors.append(f"{path}: {proto['name']} missing @brief")
|
||||
return errors
|
||||
|
||||
|
||||
def _doc_errors(path: Path, state: dict) -> list[str]:
|
||||
"""
|
||||
Apply the suffix-specific documentation rules.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
state : dict
|
||||
Scanner state with records and prototypes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
Human-readable documentation errors.
|
||||
"""
|
||||
lines = state["lines"]
|
||||
if path.suffix == ".c":
|
||||
return _c_doc_errors(path, lines, state["records"])
|
||||
return _h_doc_errors(path, lines, state["protos"])
|
||||
|
||||
|
||||
def _dash_errors(path: Path) -> list[str]:
|
||||
"""
|
||||
Find em/en dashes in one file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
Dash diagnostics.
|
||||
"""
|
||||
text = path.read_text(encoding="utf-8")
|
||||
errors = []
|
||||
if EN_DASH in text:
|
||||
errors.append(f"{path}: en dash U+2013 present")
|
||||
if EM_DASH in text:
|
||||
errors.append(f"{path}: em dash U+2014 present")
|
||||
return errors
|
||||
|
||||
|
||||
def _audit_file(path: Path) -> int:
|
||||
"""
|
||||
Audit one file and print its violations.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
One when any violation was found, otherwise zero.
|
||||
"""
|
||||
state = _scan(path)
|
||||
blank = _blank_lines(path, state["records"])
|
||||
long = [r for r in state["records"] if r["count"] > MAX_EXEC_LINES]
|
||||
docs = _doc_errors(path, state)
|
||||
dashes = _dash_errors(path)
|
||||
_report(path, blank, long, docs, dashes)
|
||||
return int(bool(blank) or bool(long) or bool(docs) or bool(dashes))
|
||||
|
||||
|
||||
def _report(path: Path, blank: list, long: list, docs: list,
|
||||
dashes: list) -> None:
|
||||
"""
|
||||
Print one file's audit findings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
blank : list
|
||||
Blank line numbers inside bodies.
|
||||
long : list
|
||||
Functions over the executable-line limit.
|
||||
docs : list
|
||||
Documentation errors.
|
||||
dashes : list
|
||||
Dash errors.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if blank:
|
||||
print(f"{path}: blank lines in body {blank}")
|
||||
for record in long:
|
||||
print(f"{path}: {record['name']} has {record['count']} exec lines")
|
||||
for error in docs:
|
||||
print(error)
|
||||
for error in dashes:
|
||||
print(error)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""
|
||||
Audit all owned C and header files.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Zero for a clean audit, otherwise one.
|
||||
"""
|
||||
failures = 0
|
||||
for path in _owned_files():
|
||||
failures += _audit_file(path)
|
||||
return int(bool(failures))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user