3"""Parse active gcovr result-masking controls and their local provenance."""
5from __future__
import annotations
8from dataclasses
import dataclass
9from pathlib
import Path
11from suppression_catalog
import is_build_control, ownership
12from suppression_hash_lex
import HashLexLine, hash_lines
13from suppression_model
import Finding, Suppression
15PARSE_ERROR_VALUES = frozenset({
"all",
"negative_hits.warn",
"negative_hits.warn_once_per_file"})
17 r"(?P<parse>--gcov-ignore-parse-errors(?:=|\s+)(?P<value>[^\s)]+))"
18 r"|(?P<unreachable>--exclude-unreachable-branches)"
19 r"|(?P<throw>--exclude-throw-branches)"
21CONFIG_KEYS = frozenset(
22 {
"gcov-ignore-parse-errors",
"exclude-unreachable-branches",
"exclude-throw-branches"}
26def _reason(line: HashLexLine) -> str:
27 """Return the same-line rationale for one active coverage mask."""
28 return line.comment.strip()
31@dataclass(frozen=True)
33 """Normalized gcovr coverage-mask fields."""
41def _record(path: str, line: HashLexLine, control: CoverageControl) -> Suppression:
42 """Build one gcovr coverage-control inventory row."""
43 concerns: list[str] = []
44 if not control.reason:
45 concerns.append(
"blank-reason")
47 concerns.append(
"broad-coverage-mask")
61 evidence=(
"producer:gcovr-7.0",),
62 recommendation=
"retain-only-with-producer-evidence",
66def _is_data_only(prefix: str) -> bool:
67 """Reject quoted examples and assignments that do not invoke gcovr."""
68 stripped = prefix.strip()
69 if re.match(
r"^(?:echo|printf|message)\b", stripped):
71 return re.match(
r"^[A-Za-z_][A-Za-z0-9_]*=", stripped)
is not None
74def _scan_flags(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
75 """Parse active shell/CMake gcovr option tokens."""
76 first_line = text.partition(
"\n")[0]
77 if not is_build_control(path, first_line):
79 lines, lex_findings = hash_lines(path, text)
80 records: list[Suppression] = []
81 findings = [Finding(item.code, item.message, path, item.line)
for item
in lex_findings]
83 for match
in FLAG_RE.finditer(line.code):
84 if _is_data_only(line.code[: match.start()]):
86 reason = _reason(line)
87 if match.group(
"parse"):
88 value = match.group(
"value").strip(
"'\"")
89 if value
not in PARSE_ERROR_VALUES:
90 message = f
"unsupported gcovr 7.0 parse-error class {value!r}"
91 findings.append(Finding(
"malformed-coverage-mask", message, path, line.line))
93 rule = f
"gcov-ignore-parse-errors={value}"
98 CoverageControl(match.start() + 1, rule, reason, value ==
"all"),
101 elif match.group(
"unreachable"):
108 "exclude-unreachable-branches",
121 "exclude-throw-branches",
127 return records, findings
130def _config_bool(value: str) -> bool |
None:
131 """Parse gcovr's accepted boolean spellings."""
132 lowered = value.lower()
133 if lowered
in {
"yes",
"true",
"1"}:
135 if lowered
in {
"no",
"false",
"0"}:
140def _scan_config(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
141 """Parse an authoritative gcovr.cfg without accepting unknown mask syntax."""
142 if Path(path).name !=
"gcovr.cfg":
144 records: list[Suppression] = []
145 findings: list[Finding] = []
146 seen: dict[str, int] = {}
147 for line_no, raw
in enumerate(text.splitlines(), start=1):
148 stripped = raw.strip()
149 if not stripped
or stripped.startswith(
"#")
or "=" not in raw:
151 key, value_and_comment = (part.strip()
for part
in raw.split(
"=", 1))
152 value, _separator, comment = value_and_comment.partition(
"#")
153 value = value.strip()
154 if key
not in CONFIG_KEYS:
157 message = f
"{key} duplicates line {seen[key]}"
158 findings.append(Finding(
"duplicate-coverage-mask", message, path, line_no))
161 if key ==
"gcov-ignore-parse-errors":
162 if value
not in PARSE_ERROR_VALUES:
163 findings.append(Finding(
"malformed-coverage-mask", f
"{key}={value}", path, line_no))
165 rule = f
"{key}={value}"
166 broad = value ==
"all"
168 active = _config_bool(value)
170 findings.append(Finding(
"malformed-coverage-mask", f
"{key}={value}", path, line_no))
176 line = HashLexLine(line_no, raw, comment.strip(), raw.find(
"#") + 1)
177 records.append(_record(path, line, CoverageControl(1, rule, comment.strip(), broad)))
178 return records, findings
181def scan_coverage_masks(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
182 """Inventory active gcovr masks from command lines and central config."""
183 records, findings = _scan_flags(path, text)
184 config_records, config_findings = _scan_config(path, text)
185 records.extend(config_records)
186 findings.extend(config_findings)
187 return records, findings