Files
smom-dbis-138/scripts/forge/report-contract-reachability.py
defiQUG 2b52cc6e32 refactor(archive): move historical contracts and adapters to archive directory
- Archived multiple non-EVM adapters (Algorand, Hedera, Tron, TON, Cosmos, Solana) and compliance contracts (IndyVerifier) to `archive/solidity/contracts/`.
- Updated documentation to reflect the historical status of archived components.
- Adjusted `foundry.toml` and `README.md` for clarity on historical dependencies and configurations.
- Enhanced Makefile and package.json scripts for improved contract testing and building processes.
- Removed obsolete contracts (AlltraCustomBridge, CommodityCCIPBridge, ISO4217WCCIPBridge, VaultBridgeAdapter) from the main directory.
- Updated implementation reports to indicate archived status for various components.
2026-04-12 18:21:05 -07:00

141 lines
5.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Report Solidity contracts that are not reachable from current tests or scripts."""
from __future__ import annotations
import argparse
import json
import re
import sys
from collections import Counter
from pathlib import Path
EXCLUDED_PARTS = {"artifacts", "broadcast", "cache", "lib", "node_modules", "out"}
IMPORT_RE = re.compile(r'^\s*import\s+(?:\{[^}]+\}\s+from\s+)?["\']([^"\']+)["\'];', re.M)
def discover_sources(repo_root: Path) -> dict[str, Path]:
return {
path.relative_to(repo_root).as_posix(): path
for path in repo_root.rglob("*.sol")
if path.is_file() and not any(part in EXCLUDED_PARTS for part in path.parts)
}
def resolve_import(importer: Path, import_path: str, repo_root: Path) -> str | None:
if import_path.startswith(("./", "../")):
target = (importer.parent / import_path).resolve()
try:
return target.relative_to(repo_root).as_posix()
except ValueError:
return None
if import_path.startswith("@emoney/"):
return f"contracts/emoney/{import_path[len('@emoney/'):]}"
return None
def build_graph(repo_root: Path, files: dict[str, Path]) -> tuple[dict[str, list[str]], dict[str, set[str]]]:
imports: dict[str, list[str]] = {}
inbound: dict[str, set[str]] = {rel: set() for rel in files}
for rel_path, file_path in files.items():
text = file_path.read_text(errors="ignore")
resolved_targets: list[str] = []
for import_path in IMPORT_RE.findall(text):
target = resolve_import(file_path, import_path, repo_root)
if target and target in files:
resolved_targets.append(target)
inbound[target].add(rel_path)
imports[rel_path] = resolved_targets
return imports, inbound
def walk_reachable(imports: dict[str, list[str]], roots: list[str]) -> set[str]:
seen: set[str] = set()
stack = list(roots)
while stack:
current = stack.pop()
if current in seen:
continue
seen.add(current)
stack.extend(imports.get(current, []))
return seen
def create_report(repo_root: Path) -> dict[str, object]:
files = discover_sources(repo_root)
imports, inbound = build_graph(repo_root, files)
contract_files = sorted(rel for rel in files if rel.startswith("contracts/"))
root_files = sorted(rel for rel in files if rel.startswith(("test/", "script/")))
reachable = walk_reachable(imports, root_files)
unreachable = [rel for rel in contract_files if rel not in reachable]
no_inbound = [rel for rel in contract_files if not inbound[rel]]
unreachable_by_bucket = Counter(path.split("/")[1] for path in unreachable)
no_inbound_by_bucket = Counter(path.split("/")[1] for path in no_inbound)
return {
"repoRoot": repo_root.as_posix(),
"summary": {
"contractsTotal": len(contract_files),
"rootsTotal": len(root_files),
"contractsReachableFromTestsOrScripts": len(contract_files) - len(unreachable),
"contractsUnreachableFromTestsOrScripts": len(unreachable),
"contractsWithNoLocalInboundRefs": len(no_inbound),
},
"unreachableByBucket": dict(unreachable_by_bucket.most_common()),
"noInboundByBucket": dict(no_inbound_by_bucket.most_common()),
"unreachableContracts": unreachable,
"contractsWithNoLocalInboundRefs": no_inbound,
}
def print_text(report: dict[str, object]) -> None:
summary = report["summary"]
print(f"repo root: {report['repoRoot']}")
print(f"contracts total: {summary['contractsTotal']}")
print(f"tests/scripts roots total: {summary['rootsTotal']}")
print(f"contracts reachable from tests or scripts: {summary['contractsReachableFromTestsOrScripts']}")
print(f"contracts unreachable from tests or scripts: {summary['contractsUnreachableFromTestsOrScripts']}")
print(f"contracts with no local inbound refs: {summary['contractsWithNoLocalInboundRefs']}")
print()
print("Unreachable by top-level bucket:")
for bucket, count in report["unreachableByBucket"].items():
print(f" {bucket}: {count}")
print()
print("Archive candidates (unreachable from current tests/scripts):")
for contract in report["unreachableContracts"]:
print(f" {contract}")
print()
print("Note: unreachable does not prove safe deletion; it only means this repo's current Solidity tests/scripts do not import the file.")
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
return parser.parse_args(argv)
def main(argv: list[str]) -> int:
args = parse_args(argv)
repo_root = Path(__file__).resolve().parents[2]
report = create_report(repo_root)
if args.json:
json.dump(report, sys.stdout, indent=2)
sys.stdout.write("\n")
return 0
print_text(report)
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))