from __future__ import annotations
import argparse
import importlib.util
import re
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import scip_reach
ID_RE = re.compile(r"AUD-[A-Z]-S\d+(?:W\d+)?-\d+")
PATH_RE = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)::([A-Za-z_][A-Za-z0-9_]*)\b")
CALL_RE = re.compile(r"\b([a-z_][a-z0-9_]{2,})\(\)")
TICK_RE = re.compile(r"`([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*)`")
NOT_ITEMS = {"self", "Self", "super", "crate", "std", "core", "alloc", "f32", "f64", "i32", "i64", "u32", "u64",
"usize", "isize", "u8", "u16", "i8", "i16", "u128", "i128", "bool", "Option", "Some", "None", "Ok",
"Err", "Result", "Vec", "MAX", "MIN", "EPSILON", "NAN", "INFINITY", "NEG_INFINITY", "ZERO", "ONE",
"true", "false", "NaN", "inf"}
def _load_oracle():
spec = importlib.util.spec_from_file_location("gen_oracle_status", HERE / "gen-oracle-status.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def defects(root: Path) -> list[dict]:
oracle = _load_oracle()
out = []
for tf in sorted((root / "tests").glob("*.rs")):
for t in oracle.extract_test_metadata(tf):
r = t["ignore_reason"]
if r.lower().startswith("known defect"):
m = ID_RE.search(r)
out.append({"id": m.group(0) if m else "", "test": t["name"], "file": t["file"], "reason": r})
return out
FILE_REF_RE = re.compile(r"[A-Za-z0-9_/.-]+\.rs(?::\d+)?(?:::[A-Za-z_][A-Za-z0-9_]*)?")
def mentions(reason: str) -> list[str]:
m = ID_RE.search(reason)
body = reason[m.end():] if m else reason body = FILE_REF_RE.sub(" ", body) body = re.split(r"root:\s*external\b", body)[0] found: list[tuple[str, bool]] = [] for m in TICK_RE.finditer(body):
found.append((m.group(1), True))
for a, b in PATH_RE.findall(body):
found.append((f"{a}::{b}", False))
for n in CALL_RE.findall(body):
found.append((n, False))
out = []
for f, ticked in found:
parts = f.split("::")
if parts[0] in NOT_ITEMS:
continue if len(parts) == 1 and parts[0] in NOT_ITEMS:
continue
if ticked and len(parts) == 1 and parts[0].islower() and "_" not in parts[0]:
continue if f not in out:
out.append(f)
return out
class Index:
def __init__(self, root: Path, scip_paths: list[Path]):
self.by_name: dict[str, list[str]] = {}
self.by_pair: dict[tuple[str, str], list[str]] = {}
for sp in scip_paths:
for doc in scip_reach.load_scip(sp):
if not doc["path"].startswith("src/"):
continue
for o in doc["occ"]:
s = o["symbol"]
if not (o["roles"] & 1) or not s or s.startswith("local "):
continue
loc = f"{doc['path']}:{o['range'][0] + 1}"
desc = s.split(" ", 4)[-1]
segs = [x for x in re.split(r"[/#.()\[\]`<>]+", desc) if x]
if not segs:
continue
name = segs[-1]
self.by_name.setdefault(name, []).append(loc)
for c in segs[:-1]:
self.by_pair.setdefault((c, name), []).append(loc)
self.text_fn: dict[str, list[str]] = {}
self.text_type: dict[str, set[str]] = {} for p in sorted((root / "src").rglob("*.rs")):
rel = p.relative_to(root).as_posix()
for i, line in enumerate(p.read_text(encoding="utf-8", errors="replace").split("\n"), 1):
for m in re.finditer(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)", line):
self.text_fn.setdefault(m.group(1), []).append(f"{rel}:{i}")
for m in re.finditer(r"\b(?:struct|enum|type|trait|union|mod)\s+([A-Za-z_][A-Za-z0-9_]*)", line):
self.text_type.setdefault(m.group(1), set()).add(rel)
for m in re.finditer(r"\b[a-z_]+!\(\s*([A-Z][A-Za-z0-9_]*)\s*,", line):
self.text_type.setdefault(m.group(1), set()).add(rel)
def resolve(self, mention: str) -> tuple[str, list[str]]:
parts = mention.split("::")
if len(parts) == 1:
n = parts[0]
if n in self.by_name:
return "scip", self.by_name[n]
if n in self.text_fn:
return "text", self.text_fn[n]
return ("text", []) if n in self.text_type else ("", [])
a, b = parts[-2], parts[-1]
if (a, b) in self.by_pair:
return "scip", self.by_pair[(a, b)]
if b in NOT_ITEMS or b.isupper(): if a in self.by_name or a in self.text_type:
return "scip", self.by_name.get(a, [])
pref = sorted({loc for (t, n), locs in self.by_pair.items() if n == b and t.startswith(a) for loc in locs})
if pref:
return "scip", pref
files = {f for t, fs in self.text_type.items() if t == a or t.startswith(a) for f in fs}
files |= {loc.rsplit(":", 1)[0] for loc in self.by_name.get(a, [])}
locs = [loc for loc in self.text_fn.get(b, []) if loc.rsplit(":", 1)[0] in files]
if locs:
return "text", locs
return "", []
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("ids", nargs="*", help="defect IDs (or test names) to look up")
ap.add_argument("--root", default=str(HERE.parent))
ap.add_argument("--scip", default="target/scip")
ap.add_argument("--check", action="store_true", help="fail when any mention does not resolve")
args = ap.parse_args(argv)
root = Path(args.root)
sdir = Path(args.scip) if Path(args.scip).is_absolute() else root / args.scip
paths = [sdir / "native.scip", sdir / "wasm.scip"]
missing = [str(p) for p in paths if not p.exists()]
if missing:
print(f"error: SCIP index missing: {missing} (run scripts/scip_index.sh)", file=sys.stderr)
return 1
idx = Index(root, paths)
ds = defects(root)
if args.ids:
ds = [d for d in ds if d["id"] in args.ids or d["test"] in args.ids]
if not ds:
print(f"error: no known defect with id {args.ids}", file=sys.stderr)
return 1
n_mentions, unresolved = 0, []
for d in ds:
ms = mentions(d["reason"])
n_mentions += len(ms)
if args.ids:
print(f"{d['id']} {d['file']}::{d['test']}")
print(f" {d['reason']}")
for m in ms:
how, locs = idx.resolve(m)
if not how:
unresolved.append((d, m))
if args.ids:
where = ", ".join(sorted(set(locs))[:4]) + (" …" if len(set(locs)) > 4 else "")
print(f" {m:40s} {'NOT FOUND' if not how else how + ': ' + where}")
print(f"compared: defects {len(ds)}, mentions {n_mentions}, unresolved {len(unresolved)}")
for d, m in unresolved:
print(f"unresolved: {d['id'] or '(no id)'} ({d['file']}::{d['test']}) names `{m}`, defined nowhere in src/",
file=sys.stderr)
if args.check:
if not ds or n_mentions == 0:
print("error: compared nothing (no known-defect reasons, or none names code)", file=sys.stderr)
return 1
return 1 if unresolved else 0
return 0
if __name__ == "__main__":
sys.exit(main())