mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-04 20:03:40 +00:00
134 lines
4.5 KiB
Python
134 lines
4.5 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import re
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from .manifest import VerificationLayout
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(slots=True)
|
||
|
|
class HygieneIssue:
|
||
|
|
severity: str
|
||
|
|
code: str
|
||
|
|
path: str
|
||
|
|
line: int
|
||
|
|
message: str
|
||
|
|
|
||
|
|
def to_dict(self) -> dict[str, object]:
|
||
|
|
return {
|
||
|
|
"severity": self.severity,
|
||
|
|
"code": self.code,
|
||
|
|
"path": self.path,
|
||
|
|
"line": self.line,
|
||
|
|
"message": self.message,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
SORRY_RE = re.compile(r"\bsorry\b")
|
||
|
|
AXIOM_RE = re.compile(r"^\s*axiom\s+[A-Za-z0-9_'.]+", re.MULTILINE)
|
||
|
|
TRUE_TARGET_RE = re.compile(r"\b(?:theorem|lemma)\s+[A-Za-z0-9_'.]+[\s\S]{0,240}?:\s*True\s*:=")
|
||
|
|
BY_TRIVIAL_RE = re.compile(r"\bby\s+trivial\b")
|
||
|
|
NATIVE_DECIDE_RE = re.compile(r"\bnative_decide\b")
|
||
|
|
|
||
|
|
|
||
|
|
def scan_hygiene(layout: VerificationLayout, certificates: list[str] | None = None) -> list[HygieneIssue]:
|
||
|
|
issues: list[HygieneIssue] = []
|
||
|
|
certs = certificates or []
|
||
|
|
seen_text = ""
|
||
|
|
for path in layout.lean_files:
|
||
|
|
rel = _rel(path, layout.verification_dir)
|
||
|
|
text = path.read_text(encoding="utf-8", errors="ignore")
|
||
|
|
code = strip_lean_comments(text)
|
||
|
|
seen_text += "\n" + code
|
||
|
|
issues.extend(_scan_file(path, rel, code))
|
||
|
|
for path in layout.files_not_in_manifest:
|
||
|
|
issues.append(
|
||
|
|
HygieneIssue(
|
||
|
|
severity="warning",
|
||
|
|
code="proof-file-not-in-manifest",
|
||
|
|
path=_rel(path, layout.verification_dir),
|
||
|
|
line=1,
|
||
|
|
message="Lean proof file is not listed in an explicit manifest.",
|
||
|
|
)
|
||
|
|
)
|
||
|
|
for cert in certs:
|
||
|
|
if cert and cert not in seen_text:
|
||
|
|
issues.append(
|
||
|
|
HygieneIssue(
|
||
|
|
severity="error",
|
||
|
|
code="missing-certificate-name",
|
||
|
|
path=str(layout.verification_dir),
|
||
|
|
line=1,
|
||
|
|
message=f"Configured certificate name was not found in scanned Lean files: {cert}",
|
||
|
|
)
|
||
|
|
)
|
||
|
|
return issues
|
||
|
|
|
||
|
|
|
||
|
|
def strip_lean_comments(text: str) -> str:
|
||
|
|
out: list[str] = []
|
||
|
|
i = 0
|
||
|
|
depth = 0
|
||
|
|
while i < len(text):
|
||
|
|
if depth == 0 and text.startswith("--", i):
|
||
|
|
while i < len(text) and text[i] != "\n":
|
||
|
|
i += 1
|
||
|
|
if i < len(text):
|
||
|
|
out.append("\n")
|
||
|
|
i += 1
|
||
|
|
continue
|
||
|
|
if text.startswith("/-", i):
|
||
|
|
depth += 1
|
||
|
|
i += 2
|
||
|
|
continue
|
||
|
|
if depth > 0:
|
||
|
|
if text.startswith("-/", i):
|
||
|
|
depth -= 1
|
||
|
|
i += 2
|
||
|
|
else:
|
||
|
|
if text[i] == "\n":
|
||
|
|
out.append("\n")
|
||
|
|
i += 1
|
||
|
|
continue
|
||
|
|
out.append(text[i])
|
||
|
|
i += 1
|
||
|
|
return "".join(out)
|
||
|
|
|
||
|
|
|
||
|
|
def _scan_file(path: Path, rel: str, code: str) -> list[HygieneIssue]:
|
||
|
|
issues: list[HygieneIssue] = []
|
||
|
|
for match in SORRY_RE.finditer(code):
|
||
|
|
issues.append(_issue("error", "sorry", rel, code, match.start(), "Executable Lean code contains 'sorry'."))
|
||
|
|
if _is_under_proofs(path) and AXIOM_RE.search(code):
|
||
|
|
for match in AXIOM_RE.finditer(code):
|
||
|
|
issues.append(_issue("error", "axiom-in-proofs", rel, code, match.start(), "Proofs/ contains an axiom declaration."))
|
||
|
|
for match in TRUE_TARGET_RE.finditer(code):
|
||
|
|
issues.append(_issue("warning", "trivial-true-target", rel, code, match.start(), "Theorem or lemma target appears to be trivially True."))
|
||
|
|
if _is_spec_or_certificate(path):
|
||
|
|
for match in BY_TRIVIAL_RE.finditer(code):
|
||
|
|
issues.append(_issue("warning", "by-trivial-in-spec", rel, code, match.start(), "Spec/certificate/root file contains 'by trivial'."))
|
||
|
|
for match in NATIVE_DECIDE_RE.finditer(code):
|
||
|
|
issues.append(_issue("advisory", "native-decide", rel, code, match.start(), "native_decide appears in proof code; review dependency cone."))
|
||
|
|
return issues
|
||
|
|
|
||
|
|
|
||
|
|
def _issue(severity: str, code_name: str, rel: str, code: str, offset: int, message: str) -> HygieneIssue:
|
||
|
|
return HygieneIssue(severity=severity, code=code_name, path=rel, line=code.count("\n", 0, offset) + 1, message=message)
|
||
|
|
|
||
|
|
|
||
|
|
def _is_under_proofs(path: Path) -> bool:
|
||
|
|
return "Proofs" in path.parts
|
||
|
|
|
||
|
|
|
||
|
|
def _is_spec_or_certificate(path: Path) -> bool:
|
||
|
|
lower = path.name.lower()
|
||
|
|
return any(token in lower for token in ("spec", "certificate", "main", "root"))
|
||
|
|
|
||
|
|
|
||
|
|
def _rel(path: Path, root: Path) -> str:
|
||
|
|
try:
|
||
|
|
return str(path.relative_to(root))
|
||
|
|
except ValueError:
|
||
|
|
return str(path)
|