- 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.
141 lines
5.0 KiB
Python
Executable File
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:]))
|