3"""Source-located clang-tidy global exclusion inventory."""
5from __future__
import annotations
10from suppression_catalog
import ownership
11from suppression_model
import Finding, Suppression
13CLANG_TIDY_REASON_RE = re.compile(
r"^\s*#\s+-(?P<rule>[A-Za-z0-9.*_-]+),?(?:\s+.*)?$")
14CLANG_TIDY_RULE_RE = re.compile(
r"[A-Za-z0-9.*_-]+")
17def _concerns(rule: str, reason: str) -> tuple[str, ...]:
18 """Return machine-observable review concerns for one global exclusion."""
19 concerns: list[str] = []
21 concerns.append(
"broad-rule")
23 concerns.append(
"blank-reason")
24 return tuple(concerns)
27def _reasons(text: str) -> dict[str, str]:
28 """Associate documented disable headings with their following comments."""
29 result: dict[str, str] = {}
30 active: list[str] = []
32 for raw
in text.splitlines():
33 if raw.startswith(
"Checks:"):
35 heading = CLANG_TIDY_REASON_RE.fullmatch(raw)
36 if heading
is not None:
38 reason =
" ".join(notes).strip()
39 result.update(dict.fromkeys(active, reason))
42 active.append(heading.group(
"rule"))
46 stripped = raw.strip()
47 if not stripped.startswith(
"#"):
49 note = stripped[1:].strip()
53 reason =
" ".join(notes).strip()
54 result.update(dict.fromkeys(active, reason))
58def scan_clang_tidy_config(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
59 """Inventory every negative clang-tidy Checks glob from active YAML."""
60 if path !=
".clang-tidy":
63 parsed = yaml.safe_load(text)
64 except yaml.YAMLError
as exc:
65 return [], [Finding(
"malformed-clang-tidy-config", str(exc), path)]
66 if not isinstance(parsed, dict)
or not isinstance(parsed.get(
"Checks"), str):
67 message =
"top-level Checks must be a YAML string"
68 return [], [Finding(
"malformed-clang-tidy-config", message, path)]
69 entries = [entry.strip()
for entry
in parsed[
"Checks"].split(
",")
if entry.strip()]
70 negative = [entry[1:].strip()
for entry
in entries
if entry.startswith(
"-")]
71 findings: list[Finding] = []
72 if any(
not rule
or CLANG_TIDY_RULE_RE.fullmatch(rule)
is None for rule
in negative):
74 Finding(
"malformed-clang-tidy-config",
"invalid negative Checks glob", path)
76 if len(negative) != len(set(negative)):
78 Finding(
"malformed-clang-tidy-config",
"duplicate negative Checks glob", path)
80 reasons = _reasons(text)
81 checks_offset = text.find(
"Checks:")
82 records: list[Suppression] = []
84 offset = text.find(
"-" + rule, checks_offset)
88 "malformed-clang-tidy-config",
89 f
"cannot source-locate negative Checks glob {rule}",
94 line = text.count(
"\n", 0, offset) + 1
95 line_start = text.rfind(
"\n", 0, offset)
96 column = offset - line_start
97 normalized =
"*" if rule ==
"*" else rule
98 reason = reasons.get(rule,
"")
112 _concerns(normalized, reason),
115 return records, findings