155 lines
5.1 KiB
Python
155 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate a source-backed FreeCAD 1.2 Document API contract inventory."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
CONTRACTS = {
|
|
"Application": {
|
|
"path": "src/App/Application.h",
|
|
"patterns": [
|
|
r"\bnewDocument\s*\(",
|
|
r"\bcloseDocument\s*\(",
|
|
r"\bgetDocument\s*\(",
|
|
r"\bcommitTransaction\s*\(",
|
|
r"\babortTransaction\s*\(",
|
|
],
|
|
},
|
|
"Document": {
|
|
"path": "src/App/Document.h",
|
|
"patterns": [
|
|
r"\baddObject\s*\(",
|
|
r"\bgetObject\s*\(",
|
|
r"\bopenTransaction\s*\(",
|
|
r"\bcommitTransaction\s*\(",
|
|
r"\babortTransaction\s*\(",
|
|
r"\brecompute\s*\(",
|
|
r"\bgetObjectList\s*\(",
|
|
r"\bgetDependencyList\s*\(",
|
|
],
|
|
},
|
|
"DocumentObject": {
|
|
"path": "src/App/DocumentObject.h",
|
|
"patterns": [
|
|
r"\bgetNameInDocument\s*\(",
|
|
r"\bgetFullName\s*\(",
|
|
r"\bgetID\s*\(",
|
|
r"\bisTouched\s*\(",
|
|
r"\bisError\s*\(",
|
|
r"\bgetInList\s*\(",
|
|
r"\bgetOutList\s*\(",
|
|
],
|
|
},
|
|
"PropertyContainer": {
|
|
"path": "src/App/PropertyContainer.h",
|
|
"patterns": [
|
|
r"\baddProperty\s*\(",
|
|
r"\bgetPropertyByName\s*\(",
|
|
r"\bgetPropertyList\s*\(",
|
|
],
|
|
},
|
|
"PartFeature": {
|
|
"path": "src/Mod/Part/App/PartFeature.h",
|
|
"patterns": [
|
|
r"\bPropertyPartShape\s+Shape\b",
|
|
r"\bgetShape\s*\(",
|
|
r"\bgetTopoShape\s*\(",
|
|
r"\bbuildHistory\s*\(",
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def collect_contract(source_root: Path, name: str, spec: dict[str, object]) -> dict[str, object]:
|
|
relative = str(spec["path"])
|
|
path = source_root / relative
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
lines = text.splitlines()
|
|
matches: list[dict[str, object]] = []
|
|
for pattern in spec["patterns"]: # type: ignore[union-attr]
|
|
regex = re.compile(str(pattern))
|
|
for number, line in enumerate(lines, start=1):
|
|
if regex.search(line):
|
|
matches.append({"pattern": str(pattern), "line": number, "text": line.strip()})
|
|
return {
|
|
"name": name,
|
|
"path": relative,
|
|
"sha256": sha256(path),
|
|
"patterns": list(spec["patterns"]), # type: ignore[arg-type]
|
|
"matches": matches,
|
|
"match_count": len(matches),
|
|
}
|
|
|
|
|
|
def render_markdown(report: dict[str, object]) -> str:
|
|
lines = [
|
|
"# FreeCAD 1.2 Document Contract",
|
|
"",
|
|
"> This is a source inventory, not a claim that the full Document runtime is available in WebAssembly.",
|
|
"",
|
|
f"- Schema: `{report['schema']}`",
|
|
f"- Source root: `{report['source_root']}`",
|
|
f"- Source revision: `{report['source_revision']}`",
|
|
"",
|
|
"| Contract | Source | Matches | SHA-256 |",
|
|
"| --- | --- | ---: | --- |",
|
|
]
|
|
for contract in report["contracts"]: # type: ignore[union-attr]
|
|
lines.append(
|
|
f"| `{contract['name']}` | `{contract['path']}` | {contract['match_count']} | `{contract['sha256']}` |"
|
|
)
|
|
lines.extend(["", "## Matched declarations", ""])
|
|
for contract in report["contracts"]: # type: ignore[union-attr]
|
|
lines.extend([f"### {contract['name']}", ""])
|
|
for match in contract["matches"]: # type: ignore[union-attr]
|
|
lines.append(f"- L{match['line']}: `{match['text']}`")
|
|
lines.append("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--source-root", type=Path, required=True)
|
|
parser.add_argument("--json-output", type=Path, required=True)
|
|
parser.add_argument("--markdown-output", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
|
|
source_root = args.source_root.resolve()
|
|
revision = "unknown"
|
|
try:
|
|
revision = subprocess.run(
|
|
["git", "-C", str(source_root), "rev-parse", "HEAD"],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout.strip()
|
|
except (OSError, subprocess.CalledProcessError):
|
|
pass
|
|
contracts = [collect_contract(source_root, name, spec) for name, spec in CONTRACTS.items()]
|
|
report = {
|
|
"schema": "freecad-web-document-contract/v1",
|
|
"source_root": str(source_root),
|
|
"source_revision": revision,
|
|
"contracts": contracts,
|
|
}
|
|
args.json_output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.json_output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
args.markdown_output.write_text(render_markdown(report), encoding="utf-8")
|
|
print(json.dumps({"schema": report["schema"], "contracts": len(contracts), "json": str(args.json_output), "markdown": str(args.markdown_output)}))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|