import argparse
import json
import re
import subprocess
import sys
from collections import Counter
from pathlib import Path
CODE_PAT = re.compile(r"(\d{4})_error")
REPORTER_PAT = re.compile(
r"\b("
r"fatalParserError|parserError|parserWarning|parserInfo"
r"|typeError|typeWarning|typeInfo"
r"|declarationError|declarationWarning|declarationInfo"
r"|fatalDeclarationError"
r"|syntaxError|syntaxWarning"
r"|unimplementedFeatureError|docstringParsingError"
r")\s*\("
r"|m_errorReporter\s*\.\s*("
r"warning|error|info|fatalError"
r"|typeError|typeWarning|typeInfo"
r"|declarationError|declarationWarning"
r"|syntaxError|syntaxWarning"
r"|parserError|parserWarning"
r"|docstringParsingError|unimplementedFeatureError"
r")\s*\("
r"|m_uniqueErrorReporter\s*\.\s*("
r"warning|error|info"
r")\s*\("
)
SEV_MAP = {
"fatalParserError": "error",
"parserError": "error",
"parserWarning": "warning",
"parserInfo": "info",
"typeError": "error",
"typeWarning": "warning",
"typeInfo": "info",
"declarationError": "error",
"fatalDeclarationError": "error",
"declarationWarning": "warning",
"declarationInfo": "info",
"syntaxError": "error",
"syntaxWarning": "warning",
"unimplementedFeatureError": "error",
"docstringParsingError": "error",
"warning": "warning",
"error": "error",
"fatalError": "error",
"info": "info",
}
def severity_from_window(window: str) -> str:
m = REPORTER_PAT.search(window)
if not m:
return "error"
call = m.group(1) or m.group(2) or m.group(3) or ""
return SEV_MAP.get(call, "error")
def shorten_path(p: str) -> str:
parts = p.replace("\\", "/").split("/")
return "/".join(parts[-2:]) if len(parts) >= 2 else p
STRING_LIT = re.compile(r'"((?:[^"\\]|\\.)*)"')
def _clean(s: str) -> str:
return s.replace('\\"', '"').replace("\\n", " ").replace("\\t", " ").strip()
def _is_noise(s: str) -> bool:
if len(s) < 4:
return True
if re.fullmatch(r"[A-Za-z_]\w*", s):
return True
return False
def extract_message(window: str, code: str) -> str:
code_pos = window.find(f"{code}_error")
if code_pos == -1:
return ""
after = window[code_pos:]
before = window[:code_pos]
same_line_before = ""
for line in window.splitlines():
if f"{code}_error" in line:
same_line_before = line[: line.index(f"{code}_error")]
break
if same_line_before:
slb_candidates = [
_clean(m.group(1))
for m in STRING_LIT.finditer(same_line_before)
if not _is_noise(_clean(m.group(1)))
]
if slb_candidates:
return slb_candidates[-1]
m = re.search(r'\bmessage\s*=\s*"((?:[^"\\]|\\.)*)"', after)
if m:
s = _clean(m.group(1))
if not _is_noise(s):
return s
candidates_after = []
for m in STRING_LIT.finditer(after):
s = _clean(m.group(1))
if not _is_noise(s):
candidates_after.append(s)
if candidates_after:
for s in candidates_after:
if s and (s[0].isupper() or s[0] in ("`", "'")):
return s
return candidates_after[0]
candidates_before = [
_clean(m.group(1))
for m in STRING_LIT.finditer(before)
if not _is_noise(_clean(m.group(1)))
]
if candidates_before:
return candidates_before[-1]
return ""
def parse_context_output(text: str) -> list[dict]:
seen: dict[str, dict] = {}
match_re = re.compile(r"^(.+?):(\d+):(.*)")
context_re = re.compile(r"^(.+?)-(\d+)-(.*)")
records: list[tuple[str, int, str, bool]] = []
for raw in text.splitlines():
if raw == "--":
continue m = match_re.match(raw)
if m:
records.append((m.group(1), int(m.group(2)), m.group(3), True))
continue
m = context_re.match(raw)
if m:
records.append((m.group(1), int(m.group(2)), m.group(3), False))
WINDOW = 6
for i, (filepath, lineno, content, is_match) in enumerate(records):
if not is_match:
continue
if not CODE_PAT.search(content):
continue
window_lines: list[tuple[int, str]] = []
for j in range(max(0, i - WINDOW), min(len(records), i + WINDOW + 1)):
fp_j, ln_j, c_j, _ = records[j]
if fp_j == filepath and abs(ln_j - lineno) <= WINDOW:
window_lines.append((ln_j, c_j))
window_lines.sort(key=lambda x: x[0])
window = "\n".join(c for _, c in window_lines)
m_code = CODE_PAT.search(content) if not m_code:
continue
code = m_code.group(1)
if int(code) == 0:
continue if code in seen:
continue
severity = severity_from_window(window)
message = extract_message(window, code)
seen[code] = {
"code": int(code),
"severity": severity,
"message": message,
"source": shorten_path(filepath),
"line": lineno,
"action": None,
}
return sorted(seen.values(), key=lambda x: x["code"])
SEARCH_DIRS = [
"libsolidity",
"liblangutil",
"libyul",
"libevmasm",
"libsolutil",
]
def run_rg(solidity_repo: Path) -> str:
dirs = [d for d in SEARCH_DIRS if (solidity_repo / d).exists()]
cmd = [
"rg",
"--no-heading",
"-n",
r"\b\d{4}_error\b",
"-C",
"6", "-S", "-g",
"*.cpp",
"-g",
"*.h",
] + dirs
result = subprocess.run(
cmd,
cwd=solidity_repo,
capture_output=True,
text=True,
)
if result.returncode not in (0, 1):
print(f"rg failed: {result.stderr}", file=sys.stderr)
sys.exit(1)
return result.stdout
def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--solidity-repo",
default="../argotorg/solidity",
help="Path to the solidity compiler source tree (default: ../argotorg/solidity)",
)
parser.add_argument(
"--output",
default="data/error_codes.json",
help="Output JSON file path (default: data/error_codes.json)",
)
parser.add_argument(
"--from-file",
help="Use a pre-captured rg output file instead of running rg",
)
args = parser.parse_args()
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
if args.from_file:
print(f"Reading rg output from {args.from_file} ...")
raw = Path(args.from_file).read_text()
else:
solidity_repo = Path(args.solidity_repo).expanduser().resolve()
if not solidity_repo.is_dir():
print(f"error: solidity repo not found at {solidity_repo}", file=sys.stderr)
sys.exit(1)
print(f"Searching {solidity_repo} ...")
raw = run_rg(solidity_repo)
print("Parsing context blocks ...")
entries = parse_context_output(raw)
counts = Counter(e["severity"] for e in entries)
no_msg = sum(1 for e in entries if not e["message"])
print(f" Total unique codes : {len(entries)}")
for sev, n in sorted(counts.items()):
print(f" {sev:8s}: {n}")
print(f" No message found : {no_msg}")
with open(output_path, "w") as f:
json.dump(entries, f, indent=2)
f.write("\n")
print(f"Written {output_path}")
if __name__ == "__main__":
main()