ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
suppression_validate.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Structural validation for normalized suppression inventory rows."""
4
5from __future__ import annotations
6
7import re
8from collections import Counter
9from pathlib import Path
10
11from suppression_model import Finding, Inventory, Suppression
12
13
14def deduplicate(inventory: Inventory) -> None:
15 """Add findings for repeated rows without hiding either inventory record."""
16 keys = (
17 (item.path, 0, 0, item.family, item.rule, item.scope)
18 if item.provenance == "central-list"
19 else item.duplicate_key()
20 for item in inventory.suppressions
21 )
22 counts = Counter(keys)
23 for key, count in sorted(counts.items()):
24 if count > 1:
25 path, line, _column, family, rule, scope = key
26 message = f"{count} identical {family}/{rule}/{scope} rows"
27 inventory.findings.append(Finding("duplicate-directive", message, path, line))
28
29
30def validate_fingerprints(inventory: Inventory) -> None:
31 """Reject ambiguous public row identities before rendering inventory."""
32 counts = Counter(item.fingerprint for item in inventory.suppressions)
33 for fingerprint, count in sorted(counts.items()):
34 if count > 1:
35 message = f"{count} rows share fingerprint {fingerprint}"
36 inventory.findings.append(Finding("duplicate-fingerprint", message))
37
38
39def validate_cppcheck_anchors(inventory: Inventory, root: Path) -> None:
40 """Reject literal cppcheck line anchors that no longer name source."""
41 for item in inventory.suppressions:
42 if item.provenance != "central-list":
43 continue
44 match = re.fullmatch(r"(?P<path>.+):(?P<line>[1-9][0-9]*)", item.scope)
45 if match is None:
46 continue
47 rel = match.group("path")
48 if any(char in rel for char in "*?["):
49 continue
50 target = root / rel
51 try:
52 line_count = len(target.read_text(encoding="utf-8", errors="replace").splitlines())
53 except OSError:
54 message = f"{item.scope} targets a missing source file"
55 inventory.findings.append(
56 Finding("dead-cppcheck-anchor", message, item.path, item.line)
57 )
58 continue
59 source_line = int(match.group("line"))
60 if source_line > line_count:
61 message = f"{item.scope} is past EOF ({line_count} lines)"
62 inventory.findings.append(
63 Finding("dead-cppcheck-anchor", message, item.path, item.line)
64 )
65
66
67def _region_identity(family: str, rules: frozenset[str]) -> tuple[str, ...]:
68 """Normalize only directional coverage suffixes for region pairing."""
69 if family == "coverage":
70 normalized = {re.sub(r"_(?:START|STOP)$", "", rule) for rule in rules}
71 return family, *sorted(normalized)
72 return family, *sorted(rules)
73
74
75def _regions_compatible(
76 family: str, start_rules: frozenset[str], end_rules: frozenset[str]
77) -> bool:
78 """Return whether one logical end closes one logical region start."""
79 return _region_identity(family, start_rules) == _region_identity(family, end_rules)
80
81
82def validate_regions(inventory: Inventory) -> None:
83 """Reject invalid regions while honoring markdownlint state snapshots."""
84 groups: dict[tuple[str, int, int, str, str, str], list[Suppression]] = {}
85 for item in inventory.suppressions:
86 if item.owner == "vendor" or item.provenance not in {
87 "inline-comment",
88 "compiler-pragma",
89 }:
90 continue
91 if not item.scope.startswith(("region-", "state-")):
92 continue
93 key = (item.path, item.line, item.column, item.family, item.tool, item.scope)
94 groups.setdefault(key, []).append(item)
95 stack_type = list[tuple[Suppression, frozenset[str]]]
96 stacks: dict[tuple[str, str, str], stack_type] = {}
97 snapshots: dict[tuple[str, str, str], stack_type] = {}
98 for group_key in sorted(groups):
99 items = groups[group_key]
100 item = items[0]
101 rules = frozenset(member.rule for member in items)
102 key = (item.path, item.family, item.tool)
103 stack = stacks.setdefault(key, [])
104 if item.scope == "state-capture":
105 snapshots[key] = list(stack)
106 elif item.scope == "state-restore":
107 stack[:] = snapshots.get(key, [])
108 elif item.scope == "region-start":
109 stack.append((item, rules))
110 elif not stack:
111 inventory.findings.append(
112 Finding(
113 "unmatched-region-end",
114 f"{item.family}/{sorted(rules)}",
115 item.path,
116 item.line,
117 )
118 )
119 else:
120 start, start_rules = stack.pop()
121 if not _regions_compatible(item.family, start_rules, rules):
122 message = f"{sorted(start_rules)} at line {start.line} closed by {sorted(rules)}"
123 inventory.findings.append(
124 Finding("mismatched-region", message, item.path, item.line)
125 )
126 for stack in stacks.values():
127 for item, rules in stack:
128 message = f"{item.family}/{sorted(rules)} has no region end"
129 inventory.findings.append(
130 Finding("unmatched-region-start", message, item.path, item.line)
131 )