3"""What ``check_doc_attachment.py`` finds, and the shapes it reasons over.
5The finding codes, the Doxygen tag grammar, and the three records the two
6passes exchange: a :class:`Finding`, a :class:`DocBlock` (one comment and the
7line it attaches to) and :class:`DocTags` (what that comment claims).
9Kept apart from both passes so the lexical pass and the AST pass cannot drift
10into disagreeing about what a doc block is -- the entire gate rests on the two
14from __future__
import annotations
17from dataclasses
import dataclass, field
24 "DOC001":
"@param names a parameter the signature does not have",
25 "DOC002":
"partially documented signature -- some parameters have no @param",
26 "DOC003":
"@return/@retval on a function returning void",
27 "DOC004":
"two doc blocks in a row with no declaration between them",
28 "DOC005":
"block names a different symbol than the one it is attached to",
29 "DOC006":
"block sits on a forward declaration whose definition is bare",
30 "DOC007":
"banned pointer-only definition-site boilerplate",
40 _T +
r"param\s*(?:\[[^\]]*\])?\s+"
41 r"([A-Za-z_][A-Za-z_0-9]*(?:\s*,\s*[A-Za-z_][A-Za-z_0-9]*)*)"
43RETURN_RE = re.compile(_T +
r"returns?\b")
44RETVAL_RE = re.compile(_T +
r"retval\b")
45COPY_RE = re.compile(_T +
r"copy(?:doc|details|brief)\b")
49EXPLICIT_REF_RE = re.compile(
50 _T +
r"(fn|struct|enum|union|def|var|typedef|class)\s+([A-Za-z_][A-Za-z_0-9]*)"
57IMPL_OF_RE = re.compile(
r"[Ii]mplementation of\s+`([A-Za-z_][A-Za-z_0-9]*)\s*\(\)`")
62STANDALONE_TAG_RE = re.compile(
63 _T +
r"(file|dir|mainpage|page|subpage|section|subsection|defgroup|addtogroup"
64 r"|ingroup|weakgroup|name|cond|endcond|example|internal|endinternal"
65 r"|copyright|brief\s*$)"
70GROUP_MARKER_RE = re.compile(
r"[@\\][{}]")
75COMMENTED_DEFINE_RE = re.compile(
r"\s*(?://+|/\*)\s*#\s*(?:define|undef)\b")
84RETURN_NOTHING_RE = re.compile(
86 r"(?:nothing|none|void|n/?a|no value"
87 r"|(?:this function |the function )?(?:never returns|does not return|no return))"
94BANNED_BOILERPLATE_RE = re.compile(
95 r"(?:see (?:the )?(?:public )?header for "
96 r"(?:the |full )?(?:documented )?(?:contract|description)"
97 r"|see header for full contract)",
102@dataclass(frozen=True)
104 """One gate finding."""
112 def render(self) -> str:
113 """One aligned report line for this finding.
115 The leading two spaces are part of the format: findings are printed
116 under a summary header, and the indent is what visually subordinates
119 return f
" {self.path}:{self.line} {self.code} {self.symbol} -- {self.detail}"
124 """A lexically-extracted ``/** ... */`` or ``/*! ... */`` block."""
130 standalone: bool =
False
134 trailing: bool =
False
139 """The claims a doc block makes, extracted once."""
141 params: list[str] = field(default_factory=list)
142 has_return: bool =
False
143 has_retval: bool =
False
144 has_copy: bool =
False
145 explicit_refs: list[tuple[str, str]] = field(default_factory=list)
146 impl_of: str |
None =
None
149def parse_tags(block_text: str) -> DocTags:
150 """Extract the claims a block makes.
152 ``@code``/``@endcode`` bodies are removed first: a usage example may
153 legitimately mention another function's parameters, and reading tags out of
154 one produces false positives.
156 body = re.sub(
r"[@\\]code\b.*?[@\\]endcode\b",
" ", block_text, flags=re.DOTALL)
157 params: list[str] = []
158 for m
in PARAM_RE.finditer(body):
159 params.extend(p.strip()
for p
in m.group(1).split(
","))
164 has_return=bool(RETURN_RE.search(RETURN_NOTHING_RE.sub(
" ", body))),
165 has_retval=bool(RETVAL_RE.search(body)),
166 has_copy=bool(COPY_RE.search(body)),
167 explicit_refs=EXPLICIT_REF_RE.findall(body),
168 impl_of=(m.group(1)
if (m := IMPL_OF_RE.search(body))
else None),