PureYAML/scripts/check-validation-coverage.sh

88 lines
3.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Validation coverage gate (validation-rules.md, "Every public type is validated or excluded").
#
# Enumerates every public type in the PureYAML target from the compiler's symbol graph (the
# authoritative public surface, not a grep heuristic), then checks each against the coverage
# manifest. Every public type MUST be classified exactly once as `validated` or `excluded`; an
# unclassified type, or a manifest entry that is no longer public, fails the build by name.
#
# Phase 1 enforces classification completeness. The framework phase additionally asserts that each
# `validated` entry is backed by a `Validation<Subject>` whose Subject is that type.
set -euo pipefail
cd "$(dirname "$0")/.."
MANIFEST="scripts/validation-coverage.tsv"
GRAPH_DIR=".build/out/symbolgraph"
if [[ ! -f "$MANIFEST" ]]; then
echo "check-validation-coverage: missing manifest $MANIFEST" >&2
exit 1
fi
# SwiftPM's own command writes the graph for every toolchain. Passing the compiler flags through
# -Xswiftc wrote nothing on Swift 6.4 and the gate then failed with no message. Its output is kept
# so a failed build says why, and a missing graph is named.
log=$(mktemp)
trap 'rm -f "$log"' EXIT
if ! swift package dump-symbol-graph >"$log" 2>&1; then
cat "$log" >&2
echo "check-validation-coverage: swift package dump-symbol-graph failed" >&2
exit 1
fi
if [[ ! -f "$GRAPH_DIR/PureYAML.symbols.json" ]]; then
cat "$log" >&2
echo "check-validation-coverage: no symbol graph at $GRAPH_DIR/PureYAML.symbols.json" >&2
exit 1
fi
python3 - "$GRAPH_DIR/PureYAML.symbols.json" "$MANIFEST" <<'PY'
import json
import sys
graph_path, manifest_path = sys.argv[1], sys.argv[2]
type_kinds = {"swift.struct", "swift.enum", "swift.class", "swift.actor", "swift.protocol"}
with open(graph_path) as handle:
graph = json.load(handle)
public_types = {
".".join(symbol["pathComponents"])
for symbol in graph["symbols"]
if symbol["kind"]["identifier"] in type_kinds and symbol.get("accessLevel") == "public"
}
classified = {}
duplicates = []
with open(manifest_path) as handle:
for raw in handle:
line = raw.split("#", 1)[0].strip()
if not line:
continue
parts = line.split("\t")
if len(parts) < 3 or parts[1] not in ("validated", "excluded"):
print(f"check-validation-coverage: malformed manifest line: {raw.rstrip()}", file=sys.stderr)
sys.exit(1)
name, kind = parts[0], parts[1]
if name in classified:
duplicates.append(name)
classified[name] = kind
errors = []
for name in sorted(public_types - set(classified)):
errors.append(f" unclassified public type (add to {manifest_path} as validated or excluded): {name}")
for name in sorted(set(classified) - public_types):
errors.append(f" stale manifest entry (no longer a public type): {name}")
for name in sorted(set(duplicates)):
errors.append(f" duplicate manifest entry: {name}")
if errors:
print("check-validation-coverage: FAILED", file=sys.stderr)
print("\n".join(errors), file=sys.stderr)
sys.exit(1)
validated = sum(1 for k in classified.values() if k == "validated")
excluded = sum(1 for k in classified.values() if k == "excluded")
print(f"check-validation-coverage: OK, {len(public_types)} public types ({validated} validated, {excluded} excluded)")
PY