#!/usr/bin/env python3 """Compare normalized lint diagnostics with a reviewed accepted-risk baseline.""" from __future__ import annotations import argparse import json import re import sys from pathlib import Path from typing import Any SCHEMA_VERSION = 1 DIAGNOSTIC_RE = re.compile( r"^(.+):(\d+):(\d+):\s+(warning|error):\s+(.+?)\s+\[([^\]]+)\]\s*$" ) ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") def normalize_path(value: str, root: Path, path_base: Path) -> str: path = Path(value) resolved = path.resolve() if path.is_absolute() else (path_base / path).resolve() try: return resolved.relative_to(root.resolve()).as_posix() except ValueError: return resolved.as_posix() def parse_location_number(value: str) -> int: try: return int(value) except ValueError as error: raise ValueError(f"invalid diagnostic location: {value!r}") from error def parse_diagnostics( text: str, root: Path, path_base: Path | None = None ) -> list[dict[str, Any]]: diagnostics: dict[tuple[str, int, int, str, str, str], dict[str, Any]] = {} for raw_line in text.splitlines(): line = ANSI_RE.sub("", raw_line) match = DIAGNOSTIC_RE.match(line) if match is None: continue path, line_number, column, severity, message, rule = match.groups() normalized = normalize_path(path, root, path_base or root) if Path(normalized).is_absolute(): continue record = { "path": normalized, "line": parse_location_number(line_number), "column": parse_location_number(column), "severity": severity, "rule": rule, "message": message, } identity = ( record["path"], record["line"], record["column"], severity, rule, message, ) diagnostics[identity] = record return sorted( diagnostics.values(), key=lambda item: ( item["path"], item["rule"], item["message"], item["line"], item["column"], ), ) def diagnostic_identity(record: dict[str, Any]) -> tuple[str, str, str, str]: return ( str(record.get("path", "")), str(record.get("severity", "")), str(record.get("rule", "")), str(record.get("message", "")), ) def validate_baseline(document: Any, tool: str) -> list[str]: errors: list[str] = [] if not isinstance(document, dict): return ["baseline must be a JSON object"] if document.get("schema_version") != SCHEMA_VERSION: errors.append(f"unsupported schema_version: {document.get('schema_version')}") if document.get("tool") != tool: errors.append(f"baseline tool must be {tool!r}") for field in ("reviewed_at", "reviewer"): if not isinstance(document.get(field), str) or not document[field].strip(): errors.append(f"{field} must be a non-empty string") rationales = document.get("rationales", {}) if not isinstance(rationales, dict) or not all( isinstance(key, str) and key.strip() and isinstance(value, str) and value.strip() for key, value in rationales.items() ): errors.append("rationales must map non-empty names to non-empty text") rationales = {} findings = document.get("findings") if not isinstance(findings, list): errors.append("findings must be an array") return errors seen: set[tuple[str, str, str, str]] = set() for index, finding in enumerate(findings): if not isinstance(finding, dict): errors.append(f"findings[{index}] must be an object") continue for field in ("path", "severity", "rule", "message"): if not isinstance(finding.get(field), str) or not finding[field].strip(): errors.append(f"findings[{index}].{field} must be a non-empty string") rationale = finding.get("rationale") rationale_ref = finding.get("rationale_ref") if not ( isinstance(rationale, str) and rationale.strip() or isinstance(rationale_ref, str) and rationale_ref in rationales ): errors.append( f"findings[{index}] requires a non-empty rationale or valid rationale_ref" ) if finding.get("status") != "accepted-risk": errors.append(f"findings[{index}].status must be 'accepted-risk'") identity = diagnostic_identity(finding) if identity in seen: errors.append(f"duplicate baseline diagnostic at findings[{index}]") seen.add(identity) return errors def compare_diagnostics( current: list[dict[str, Any]], baseline: dict[str, Any] ) -> dict[str, Any]: current_by_id: dict[tuple[str, str, str, str], list[dict[str, Any]]] = {} for item in current: current_by_id.setdefault(diagnostic_identity(item), []).append(item) reviewed_by_id: dict[tuple[str, str, str, str], list[dict[str, Any]]] = {} for item in baseline.get("findings", []): reviewed_by_id.setdefault(diagnostic_identity(item), []).append(item) new: list[dict[str, Any]] = [] persisting: list[dict[str, Any]] = [] resolved: list[dict[str, Any]] = [] for identity in sorted(set(current_by_id) | set(reviewed_by_id)): current_items = current_by_id.get(identity, []) reviewed_items = reviewed_by_id.get(identity, []) matched = min(len(current_items), len(reviewed_items)) persisting.extend(current_items[:matched]) new.extend(current_items[matched:]) resolved.extend(reviewed_items[matched:]) return { "schema_version": 1, "tool": baseline["tool"], "status": "failed" if new else "ok", "counts": { "current": len(current), "reviewed": sum(len(items) for items in reviewed_by_id.values()), "new": len(new), "persisting": len(persisting), "resolved": len(resolved), }, "new": new, "persisting": persisting, "resolved": resolved, "baseline": { "reviewed_at": baseline["reviewed_at"], "reviewer": baseline["reviewer"], }, } def read_exit_status(path: Path) -> int: try: value = path.read_text(encoding="utf-8").strip() return int(value) except OSError as error: raise ValueError( f"failed to read analyzer exit status {path}: {error}" ) from error except ValueError as error: raise ValueError(f"invalid analyzer exit status in {path}") from error def load_json(path: Path) -> Any: try: return json.loads(path.read_text(encoding="utf-8")) except OSError as error: raise ValueError(f"failed to read {path}: {error}") from error except json.JSONDecodeError as error: raise ValueError(f"invalid JSON in {path}: {error}") from error def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--tool", choices=("clang-tidy", "shellcheck"), required=True) parser.add_argument("--baseline", type=Path, required=True) parser.add_argument("--input", type=Path, required=True) parser.add_argument("--analyzer-exit-file", type=Path) parser.add_argument("--root", type=Path, default=Path.cwd()) parser.add_argument( "--path-base", type=Path, help="directory used to resolve relative diagnostic paths (defaults to --root)", ) parser.add_argument("--output", type=Path) args = parser.parse_args() try: baseline = load_json(args.baseline) errors = validate_baseline(baseline, args.tool) if errors: raise ValueError("; ".join(errors)) text = args.input.read_text(encoding="utf-8", errors="replace") current = parse_diagnostics(text, args.root, args.path_base) summary = compare_diagnostics(current, baseline) analyzer_exit = ( read_exit_status(args.analyzer_exit_file) if args.analyzer_exit_file is not None else 0 ) accepted_exits = {0, 1} if args.tool == "shellcheck" else {0} summary["analyzer_exit"] = analyzer_exit if analyzer_exit not in accepted_exits: summary["status"] = "failed" summary["harness_error"] = ( f"{args.tool} exited {analyzer_exit}; inspect the complete log" ) except (OSError, ValueError) as error: print(f"lint baseline error: {error}", file=sys.stderr) return 2 rendered = json.dumps(summary, indent=2, sort_keys=True) + "\n" if args.output is not None: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(rendered, encoding="utf-8") print(rendered, end="") if "harness_error" in summary: return 2 return 1 if summary["status"] == "failed" else 0 if __name__ == "__main__": raise SystemExit(main())