3"""Reading source text back, for the checks a parse cannot answer.
5Most of this checker reasons over libclang's AST. Two things cannot be:
7* the ``section("...vectors")`` attribute, because ``CursorKind.SECTION_ATTR``
8 is absent from the libclang 18.1.x wheels the runners install, so an AST
9 lookup silently finds nothing; and
10* the NSC range-check idiom, because ``RA8_NSC_CHECK_NS_RANGE_R/_RW`` expands
11 to ``cmse_check_address_range()`` only under ``-mcmse``. This script parses
12 without it, so the macro becomes a ``((void)(p),(void)(n))`` no-op and leaves
13 no ``CallExpr`` at all.
15Both therefore read the pre-preprocessor text. That is a deliberate exception,
16not a general licence to grep instead of parse, and it is confined to this
17module so the exception stays countable.
20from __future__
import annotations
24from annot_model
import AnnotatedSymbol
27_SOURCE_CACHE: dict[str, list[str]] = {}
30def source_lines(path: str) -> list[str]:
31 """Return ``path``'s lines, cached. Empty list when unreadable."""
32 if path
not in _SOURCE_CACHE:
34 _SOURCE_CACHE[path] = pathlib.Path(path).read_text(errors=
"ignore").splitlines()
36 _SOURCE_CACHE[path] = []
37 return _SOURCE_CACHE[path]
40def definition_text(sym: AnnotatedSymbol) -> str:
41 """Return the source text of ``sym``'s definition.
43 Used for checks that must see the code *before* preprocessing, because
44 the construct being looked for is a macro that this script's parse
45 configuration expands away (see the NSC range-check rule).
47 lines = source_lines(sym.file)
48 if not lines
or sym.line <= 0:
50 end = sym.end_line
if sym.end_line >= sym.line
else len(lines)
51 return "\n".join(lines[sym.line - 1 : end])