from __future__ import annotations
import argparse
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
BASELINE = "docs/architecture/public-api.txt"
CRATE_PATH = re.compile(r"\bmacrame(?:::[A-Za-z0-9_]+)+")
LEADING = re.compile(r"^macrame(?:::[A-Za-z0-9_]+)*")
PREFIXES = (
"pub async unsafe fn ",
"pub unsafe fn ",
"pub async fn ",
"pub const fn ",
"pub struct ",
"pub enum ",
"pub trait ",
"pub union ",
"pub const ",
"pub type ",
"pub macro ",
"pub fn ",
"pub ",
)
def strip_modules(path: str) -> str:
segs = path.split("::")[1:]
if not segs:
return "macrame"
i = 0
while i < len(segs) - 1 and segs[i][:1].islower():
i += 1
return "::".join(segs[i:])
def collapse_inner(text: str) -> str:
return CRATE_PATH.sub(lambda m: m.group(0).rsplit("::", 1)[-1], text)
def identity(line: str) -> str:
for prefix in PREFIXES:
if line.startswith(prefix):
rest = line[len(prefix) :]
break
else:
return collapse_inner(line)
m = LEADING.match(rest)
if not m:
return prefix + collapse_inner(rest)
return prefix + strip_modules(m.group(0)) + collapse_inner(rest[m.end() :])
class Surface:
def __init__(self, text: str, label: str) -> None:
self.label = label
self.lines = [l for l in text.splitlines() if l.strip()]
self.modules: set[str] = set()
self.items: set[str] = set()
self.non_exhaustive: set[str] = set()
self.paths = 0
for line in self.lines:
if line.startswith("pub mod "):
self.modules.add(
line[len("pub mod macrame") :].lstrip(":") or "(root)"
)
continue
if line.startswith("impl ") or line.startswith("pub impl "):
continue
flagged = line.startswith("#[non_exhaustive] ")
if flagged:
line = line[len("#[non_exhaustive] ") :]
ident = identity(line)
self.paths += 1
self.items.add(ident)
if flagged:
self.non_exhaustive.add(ident)
@property
def surplus(self) -> int:
return self.paths - len(self.items)
def read_tag(tag: str) -> str:
out = subprocess.run(
["git", "show", f"{tag}:{BASELINE}"],
cwd=ROOT,
capture_output=True,
text=True,
encoding="utf-8",
)
if out.returncode != 0:
sys.exit(
f"cannot read {BASELINE} at {tag}: {out.stderr.strip()}\n"
"The baseline has been checked in since 0.13.32; before that this "
"script cannot help and the 0.14.0 review's worktree method is the "
"only one."
)
return out.stdout
def report(old: Surface, new: Surface) -> str:
out: list[str] = []
w = out.append
w(f"{old.label:<8}: {len(old.lines)} lines, {len(old.modules)} modules, "
f"{len(old.items)} distinct items")
w(f"{new.label:<8}: {len(new.lines)} lines, {len(new.modules)} modules, "
f"{len(new.items)} distinct items")
w(f"net lines: {len(new.lines) - len(old.lines):+d} "
f"net items: {len(new.items) - len(old.items):+d}")
w("")
gone_mods = sorted(old.modules - new.modules)
new_mods = sorted(new.modules - old.modules)
w(f"--- modules ({len(old.modules)} -> {len(new.modules)}) ---")
w(f"demoted: {len(gone_mods)} new: {len(new_mods)}")
for m in gone_mods:
w(f" - {m}")
for m in new_mods:
w(f" + {m}")
w("")
ne_added = sorted(new.non_exhaustive - old.non_exhaustive)
ne_gone = sorted(old.non_exhaustive - new.non_exhaustive)
w(f"--- non_exhaustive ({len(old.non_exhaustive)} -> "
f"{len(new.non_exhaustive)}) ---")
for i in ne_gone:
w(f" - {i}")
for i in ne_added:
w(f" + {i}")
w("")
w(f"--- surplus paths on items present in both: {old.surplus} -> "
f"{new.surplus} ---")
w("")
removed = sorted(old.items - new.items)
added = sorted(new.items - old.items)
w(f"=== REMOVED FROM THE SURFACE: {len(removed)} ===")
for i in removed:
w(f" {i}")
w("")
w(f"=== ADDED TO THE SURFACE: {len(added)} ===")
for i in added:
w(f" {i}")
return "\n".join(out)
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("old", help="git tag or revision to compare against")
ap.add_argument(
"--new",
default=None,
help="git revision for the new side; default is the working tree",
)
ap.add_argument("--out", default=None, help="write the report to this file")
args = ap.parse_args()
old = Surface(read_tag(args.old), args.old)
if args.new:
new = Surface(read_tag(args.new), args.new)
else:
new = Surface((ROOT / BASELINE).read_text(encoding="utf-8"), "working")
text = report(old, new)
if args.out:
Path(args.out).write_text(text + "\n", encoding="utf-8", newline="\n")
print(f"wrote {args.out}")
else:
print(text)
if __name__ == "__main__":
main()