packages feed

exchangealgebra-0.6.0.0: tools/check-orphans.py

#!/usr/bin/env python3
"""Reject orphan instance warnings that are absent from the P2 baseline."""

import argparse
from collections import Counter
import os
import pathlib
import re
import shlex
import subprocess
import sys


ROOT = pathlib.Path(__file__).resolve().parent.parent
BASELINE = ROOT / "test/fixtures/export-surface-p2/orphans.txt"


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--update", action="store_true", help="Write the current warning set")
    args = parser.parse_args()
    command = ["stack", *shlex.split(os.environ.get("STACK_ARGS", "")),
               "--stack-yaml", str(ROOT / "stack.yaml"), "build"]
    if os.environ.get("EA_VISUALIZE", "1") == "0":
        command.extend(["exchangealgebra", "--test", "--no-run-tests"])
        command.extend(["--flag", "exchangealgebra:-visualize"])
    else:
        command.extend(["--test", "--bench", "--no-run-tests",
                        "--no-run-benchmarks"])
    command.extend(["--force-dirty", "--ghc-options=-fforce-recomp -Worphans"])
    result = subprocess.run(command, cwd=ROOT, text=True,
                            stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    if result.returncode:
        print(result.stdout, file=sys.stderr)
        return result.returncode
    warnings = set()
    blocks = re.split(r"(?=^.*?:\d+:\d+: warning:.*\[-Worphans\])",
                      result.stdout, flags=re.M)
    for block in blocks:
        header = re.match(r"^(.+?):\d+:\d+: warning:.*\[-Worphans\]", block)
        if not header:
            continue
        lines = block.splitlines()
        marker = next((index for index, line in enumerate(lines)
                       if re.search(r"Orphan .* instance:", line)), None)
        if marker is None:
            raise RuntimeError(f"Cannot parse orphan warning: {block[:250]}")
        first = lines[marker].split(" instance:", 1)[1].strip()
        declaration_parts = [first] if first else []
        for line in lines[marker + 1:]:
            detail = re.sub(r"^[^\s>]+\s*>\s*", "", line).strip()
            if detail.startswith(("Suggested fix:", "|")) or re.match(r"^\d+\s*\|", detail):
                break
            if detail:
                declaration_parts.append(detail)
        declaration = " ".join(declaration_parts)
        if not declaration.startswith(("instance ", "type instance ", "data instance ")):
            raise RuntimeError(f"Cannot parse orphan declaration: {block[:250]}")
        source = re.sub(r"^[^\s>]+\s*>\s*", "", header.group(1)).strip()
        path = pathlib.Path(source).resolve().relative_to(ROOT)
        warnings.add(f"{path}: {declaration}")
    if args.update:
        BASELINE.parent.mkdir(parents=True, exist_ok=True)
        BASELINE.write_text("".join(item + "\n" for item in sorted(warnings)))
    elif not BASELINE.exists():
        raise RuntimeError(f"Missing orphan baseline: {BASELINE}")
    else:
        baseline = set(BASELINE.read_text().splitlines())
        known_instances = Counter(line.split(": ", 1)[1] for line in baseline)
        current_instances = Counter(warning.split(": ", 1)[1] for warning in warnings)
        added = {instance: count - known_instances[instance]
                 for instance, count in current_instances.items()
                 if count > known_instances[instance]}
        if added:
            print("New orphan instances:", *(
                f"{instance}: +{added[instance]}" for instance in sorted(added)),
                sep="\n", file=sys.stderr)
            return 1
    print(f"[PASS] {len(warnings)} orphan instances; no additions")
    return 0


if __name__ == "__main__":
    sys.exit(main())