3"""The linkage rule: every non-static function states why it has external linkage.
5Separate from the other rules because it is the only one defined over the
6*absence* of an annotation. Every rule in :mod:`annot_rules` starts from a tag
7someone wrote and checks a property of it; this one starts from the whole
8symbol table and asks which definitions nobody has classified at all. That
9makes it the rule most likely to look clean by simply not running -- so it also
10owns the largest share of the selftest.
13from __future__
import annotations
17from annot_model
import AnnotatedSymbol, DataSymbol, Violation
18from annot_rulekeys
import LINKAGE_ANNOTATIONS, parse_annotation
19from annot_scope
import SOURCE_SUFFIXES, is_first_party, relative
23INTERNAL_HEADER_SUFFIX =
"_internal.h"
32STATIC_FUNCTION_PREFIX =
"internal_"
33PRIVATE_FUNCTION_PREFIX =
"priv_"
34STATIC_DATA_PREFIX =
"s_"
37def _is_published_inline_definition(sym: AnnotatedSymbol) -> bool:
38 """True for a static inline API body intentionally defined in public ``inc``."""
39 path = pathlib.PurePath(relative(sym.file))
41 path.suffix
in {
".h",
".hpp"}
42 and INTERNAL_HEADER_SUFFIX
not in path.name
43 and "inc" in path.parts
48def _linkage_keys(sym: AnnotatedSymbol) -> set[str]:
49 """Return the linkage annotations attached to ``sym``."""
51 parse_annotation(annotation)[0]
52 for annotation
in sym.annotations
53 if parse_annotation(annotation)[0]
in LINKAGE_ANNOTATIONS
57def _naming_at(sym: AnnotatedSymbol, message: str) -> Violation:
58 """Build one function naming finding at its definition site."""
59 return Violation(
"ra8_naming", sym.file, sym.line, message)
62def _annotation_shape(sym: AnnotatedSymbol, linkage: set[str]) -> list[Violation]:
63 """Check that every linkage annotation agrees with storage and prefix."""
64 out: list[Violation] = []
69 f
"function '{sym.name}' carries conflicting linkage annotations: "
70 f
"{', '.join(sorted(linkage))}",
73 if "ra8_internal" in linkage
and not sym.name.startswith(STATIC_FUNCTION_PREFIX):
75 _naming_at(sym, f
"RA8_INTERNAL function '{sym.name}' must use the internal_ prefix")
77 if "ra8_priv" in linkage
and sym.is_static:
78 out.append(_naming_at(sym, f
"RA8_PRIV function '{sym.name}' must not be static"))
79 if "ra8_priv" in linkage
and not sym.name.startswith(PRIVATE_FUNCTION_PREFIX):
80 out.append(_naming_at(sym, f
"RA8_PRIV function '{sym.name}' must use the priv_ prefix"))
81 if "ra8_test_helper" in linkage
and sym.is_static:
82 out.append(_naming_at(sym, f
"RA8_TEST_HELPER function '{sym.name}' must not be static"))
86def _static_shape(sym: AnnotatedSymbol, linkage: set[str]) -> list[Violation]:
87 """Check a non-public file-local function's storage, annotation, and prefix."""
88 if not sym.has_internal_linkage
or _is_published_inline_definition(sym):
90 out: list[Violation] = []
92 out.append(_naming_at(sym, f
"file-local function '{sym.name}' must be declared static"))
93 if "ra8_internal" not in linkage:
94 out.append(_naming_at(sym, f
"file-local function '{sym.name}' must carry RA8_INTERNAL"))
95 if not sym.name.startswith(STATIC_FUNCTION_PREFIX):
97 _naming_at(sym, f
"file-local function '{sym.name}' must use the internal_ prefix")
102def _reserved_prefix_shape(sym: AnnotatedSymbol, linkage: set[str]) -> list[Violation]:
103 """Reserve ``internal_`` and ``priv_`` for their exact linkage shapes."""
104 out: list[Violation] = []
105 if sym.name.startswith(STATIC_FUNCTION_PREFIX):
106 if not sym.is_static:
108 _naming_at(sym, f
"function '{sym.name}' uses internal_ but is not declared static")
110 if "ra8_internal" not in linkage:
112 _naming_at(sym, f
"function '{sym.name}' uses internal_ but lacks RA8_INTERNAL")
114 if sym.name.startswith(PRIVATE_FUNCTION_PREFIX)
and "ra8_priv" not in linkage:
115 out.append(_naming_at(sym, f
"function '{sym.name}' uses priv_ but lacks RA8_PRIV"))
119def _private_declaration_shape(sym: AnnotatedSymbol, linkage: set[str]) -> list[Violation]:
120 """Require an RA8_PRIV contract to be private and published nowhere else."""
121 if "ra8_priv" not in linkage:
123 out: list[Violation] = []
128 f
"RA8_PRIV function '{sym.name}' has no declaration in a module *_internal.h",
131 public_decl = published_header(sym)
132 if public_decl
is not None:
136 f
"RA8_PRIV function '{sym.name}' is also published by {relative(public_decl)}",
142def _function_naming(sym: AnnotatedSymbol) -> list[Violation]:
143 """Return every prefix/storage finding for one function definition."""
144 linkage = _linkage_keys(sym)
145 out: list[Violation] = []
146 if sym.name.startswith(STATIC_DATA_PREFIX):
150 f
"function '{sym.name}' uses the s_ prefix reserved for file-scope static data",
153 out.extend(_annotation_shape(sym, linkage))
154 out.extend(_static_shape(sym, linkage))
155 out.extend(_reserved_prefix_shape(sym, linkage))
156 out.extend(_private_declaration_shape(sym, linkage))
160def _data_naming(data: DataSymbol) -> Violation |
None:
161 """Return the prefix/scope finding for one data definition, if any."""
162 correctly_static = data.is_file_scope
and data.has_internal_linkage
163 if correctly_static
and not data.name.startswith(STATIC_DATA_PREFIX):
168 f
"file-scope static data '{data.name}' must use the s_ prefix",
170 if data.name.startswith(STATIC_DATA_PREFIX)
and not correctly_static:
175 f
"data '{data.name}' uses s_ but is not file-scope with internal linkage",
180def _naming_violations(
181 symbols: dict[str, AnnotatedSymbol], data_symbols: dict[str, DataSymbol]
183 """Enforce and source-site-deduplicate the style-guide naming contract."""
186 for sym
in symbols.values()
187 if sym.is_defined
and is_first_party(sym.file)
188 for finding
in _function_naming(sym)
192 for data
in data_symbols.values()
193 if is_first_party(data.file)
194 if (finding := _data_naming(data))
is not None
196 unique: dict[tuple[str, int, str], Violation] = {}
197 for finding
in findings:
198 unique.setdefault((finding.file, finding.line, finding.message), finding)
199 return list(unique.values())
202def published_header(sym: AnnotatedSymbol) -> str |
None:
203 """Return the header that publishes ``sym``, or None when none does.
205 A prototype in a header is an exported contract: a library's public
206 ``inc/`` header, an application's local header, a mock's header, or a
207 vendored SOUP header whose interface the function implements. A
208 prototype in a ``.c`` is a forward declaration and publishes nothing,
209 and an ``*_internal.h`` declaration is explicitly library-private --
210 both leave the function needing a linkage annotation.
212 "Header" is anything that is not a translation unit, rather than a
213 list of header suffixes: the C++ standard library headers that declare
214 the replaceable ``operator new`` / ``operator delete`` are spelled
215 ``<new>``, with no suffix at all.
217 for path
in sorted(sym.decl_files):
218 name = pathlib.PurePath(path).name
219 if pathlib.PurePath(name).suffix
in SOURCE_SUFFIXES:
221 if name.endswith(INTERNAL_HEADER_SUFFIX):
228 """Return the ``*_internal.h`` declaring ``sym``, or None."""
229 for path
in sorted(sym.decl_files):
230 if pathlib.PurePath(path).name.endswith(INTERNAL_HEADER_SUFFIX):
235def _linkage_verdict(sym: AnnotatedSymbol, vector_entries: set[str]) -> Violation |
None:
236 """Return the linkage violation for ``sym``, or None when it passes."""
237 if any(parse_annotation(a)[0]
in LINKAGE_ANNOTATIONS
for a
in sym.annotations):
239 if published_header(sym)
is not None:
241 if sym.usr
in vector_entries
or sym.name == C_ENTRY_POINT:
244 if internal
is not None:
249 f
"'{sym.name}' is declared in {relative(internal)} but carries no "
250 f
"linkage annotation; a symbol that is non-static only so one "
251 f
"library's other TUs can reach it is RA8_PRIV (or RA8_TEST_HELPER "
252 f
"when only tests call it)",
258 f
"'{sym.name}' has external linkage but nothing publishes it: no header "
259 f
"declares it and no vector table names it. Make it static and tag it "
260 f
"RA8_INTERNAL, or declare it -- in the library's inc/ header if it is "
261 f
"API, in an *_internal.h with RA8_PRIV if it is shared inside the library",
266 symbols: dict[str, AnnotatedSymbol],
267 vector_entries: set[str],
268 data_symbols: dict[str, DataSymbol] |
None =
None,
270 naming_contract: bool =
False,
272 """Every non-static function must state why it has external linkage.
274 CLAUDE.md, "Which linkage annotation to use", makes this a tree-wide
275 expectation rather than an opt-in: a function that is not `static`
276 either publishes a contract other code may call, or it is deliberately
277 non-`static` for one narrow reason that has to be written down.
279 A definition passes when any of the following holds.
281 * It carries `RA8_PRIV`, `RA8_INTERNAL` or `RA8_TEST_HELPER`.
282 * A header publishes it. A prototype in a `.h` is an exported
283 contract -- a library's public `inc/` header, an application's local
284 header, a mock's header, or the vendored SOUP header whose interface
285 the function implements (the NimBLE `ble_npl_*` porting layer, the
286 ThreadX `tx_application_define` hook, the USBX class entry points).
287 An `*_internal.h` prototype does not count: that header exists to
288 say "library-private", which is what `RA8_PRIV` marks.
289 * It is a hardware vector-table entry -- see `is_vector_table()`. The
290 CPU reaches these through VTOR with no C caller at all, so no
291 annotation describes them and no header can publish them: the only
292 thing that names an IRQ trampoline is the table slot itself. The
293 category is derived from the table's structure, so a handler that is
294 removed from the table stops being exempt the moment it is unwired.
295 * It is `main`, the ISO C entry point.
297 Anything else is a gap, and the two shapes need different fixes, so
298 they get different messages.
300 A verdict is reached per *definition site*, not per symbol name. The
301 coverage tests reach a module's `static` helpers by `#define`-ing a
302 function to a `_cov` spelling and then `#include`-ing the `.c`, so one
303 source construct shows up under two names with two USRs and the same
304 file and line. Judging the renamed copy on its own would report the
305 original function as an unpublished symbol in a file that never
308 out = _naming_violations(symbols, data_symbols
or {})
if naming_contract
else []
309 verdicts: dict[tuple[str, int], list[Violation |
None]] = {}
310 for sym
in symbols.values():
311 if not sym.is_defined
or sym.has_internal_linkage
or not is_first_party(sym.file):
313 verdicts.setdefault((sym.file, sym.line), []).append(_linkage_verdict(sym, vector_entries))
314 for site
in sorted(verdicts):
315 found = [v
for v
in verdicts[site]
if v
is not None]
316 if len(found) == len(verdicts[site])
and found:
static ra8_err_t internal_header(const ra8_fmt_sink_t *report, const ra8_fmt_source_t *source, const book_chunked_t *reader)
Emit the established RBKC header block.