4"""extract_line_citations.py -- Emit a CSV of in-tree line citations.
6Companion to scripts/checks/check_line_citations.py. Walks the same set
7of files (libs/, src/, tests/, examples/, port/), finds every
8`<file>.<ext>:<line>` token inside C/C++ comments, and emits one row
11 file,line_number,matched_citation,enclosing_function,
12 suggested_replacement,full_comment_snippet
14`enclosing_function` is the most recent function definition signature
15above the citation in the same file (or "(file scope)" if none).
17`suggested_replacement` resolves the citation target -- opens
18`<file>` at `<line>` and reports `<basename>::<func_name>` so the
19caller can rewrite `libs/foo/src/bar.c:776` as `bar.c::priv_foo`.
21Output: CSV on stdout. One-shot agent tool, not wired into pre-commit.
24from __future__
import annotations
30from collections.abc
import Iterator
31from pathlib
import Path
33sys.path.insert(0, str(Path(__file__).resolve().parent))
35from line_citation_lex
import (
39 find_mcdc_reason_spans,
43from line_citation_lex
import is_in_scope
as _lex_in_scope
47 "apps/shared_libs/third_party/",
53FUNC_DEF_RE = re.compile(
r"^[A-Za-z_][\w\s\*]*?\b([A-Za-z_]\w*)\s*\([^;]*?\)\s*\{?\s*$")
68def enclosing_function(lines: list[str], target_line: int) -> str:
69 """Walk backward from target_line looking for a function definition."""
70 for idx
in range(
min(target_line - 1, len(lines) - 1), -1, -1):
73 stripped = line.strip()
74 if not stripped
or stripped.startswith((
"//",
"/*",
"*",
"#")):
76 m = FUNC_DEF_RE.match(line)
80 if name
in FUNC_DEF_DENYLIST:
83 if "=" in line.split(
"(")[0]:
89_FILE_CACHE: dict[Path, list[str]] = {}
92def file_lines(path: Path) -> list[str] |
None:
93 """Cached line list for a file, or None when it cannot be read.
95 Cached because resolving the enclosing function for each citation re-reads
96 the same few files repeatedly; the cache turns that quadratic re-read into
99 if path
in _FILE_CACHE:
100 return _FILE_CACHE[path]
102 text = path.read_text(encoding=
"utf-8", errors=
"replace")
105 lines = text.splitlines()
106 _FILE_CACHE[path] = lines
110def resolve_target(repo_root: Path, citation_path: str, citation_line: int) -> str:
111 """Try to find the target file and report basename::func."""
113 candidates: list[Path] = []
114 p = repo_root / citation_path
119 base = Path(citation_path).name
120 for f
in all_tracked_files():
121 if Path(f).name == base
and is_in_scope(f):
122 candidates.append(repo_root / f)
125 return f
"{Path(citation_path).name}::(unknown)"
126 target = candidates[0]
127 lines = file_lines(target)
129 return f
"{target.name}::(unreadable)"
130 func = enclosing_function(lines, citation_line)
131 return f
"{target.name}::{func}"
134def is_in_scope(path: str) -> bool:
135 """Whether a source path is subject to the ban, with this tool's exclusions."""
136 return _lex_in_scope(path, EXCLUDE_PREFIXES)
139def _rows_for_file(repo_root: Path, rel: str) -> Iterator[list[object]]:
140 """Yield one CSV row per non-exempt line-citation in ``rel``.
142 Exemptions come from line_citation_lex.is_exempt -- the same predicate the
143 gate applies -- so this aid cannot list work the gate does not want, nor
144 miss work it does. It used to carry its own hand-copied cascade.
146 path = repo_root / rel
147 if not path.is_file():
150 text = path.read_text(encoding=
"utf-8", errors=
"replace")
153 lines = text.splitlines()
154 seen: set[tuple[int, str]] = set()
158 for start, end
in find_comment_spans(text) + find_mcdc_reason_spans(text):
159 for m
in CITATION_RE.finditer(text[start:end]):
160 line_no = line_of_offset(text, start + m.start())
161 abs_off = start + m.start()
163 line = lines[line_no - 1]
if 1 <= line_no <= len(lines)
else ""
164 line_start = text.rfind(
"\n", 0, abs_off) + 1
165 if is_exempt(matched, line, abs_off - line_start):
167 if (line_no, matched)
in seen:
169 seen.add((line_no, matched))
174 enclosing_function(lines, line_no),
175 resolve_target(repo_root, m.group(1), int(m.group(2))),
181 """List every in-tree ``file:line`` citation with the function that encloses it.
183 A migration aid, not a gate: check_line_citations is what FAILS on these,
184 and this prints the same set together with the symbol name each one should
185 be rewritten to cite. Exits 0 regardless of what it finds.
189 [
"git",
"rev-parse",
"--show-toplevel"],
196 files = [f
for f
in all_tracked_files()
if is_in_scope(f)]
198 writer = csv.writer(sys.stdout)
204 "enclosing_function",
205 "suggested_replacement",
206 "full_comment_snippet",
212 for row
in _rows_for_file(repo_root, f):
216 print(f
"# {rows} rows", file=sys.stderr)
220if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.