Skip to content

Commit 1971e64

Browse files
committed
fix: repair Slither naming codemod fallout and NatSpec drift
- Remove mismatched /// @PARAM lines from src/ (exclude hacks); add strip_natspec_params.py - Fix undeclared identifiers and param/shadow self-assignments (MultiSigWallet, Array, Delegatecall, SimpleStorage, Call, etc.) - Align tests with renamed getters (ERC20Permit, GasGolf, Immutable) - Document cleanup in PROGRESS.md Made-with: Cursor
1 parent fb76d70 commit 1971e64

98 files changed

Lines changed: 782 additions & 1034 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

PROGRESS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ A hands-on Solidity training ground based on solidity-by-example.org.
66

77
## Completed
88

9+
- [x] **Naming / NatSpec cleanup (2026-04)**: After Slither-oriented renames, fixed shadowing bugs (`value = value`, `num = num`), aligned bodies with new parameter names, removed drift-prone `/// @param` lines under `src/` (excluding `src/hacks/`), added `scripts/strip_natspec_params.py`, and updated tests (`ERC20Permit`, `GasGolf`, `Immutable`). Full suite green via `forge test` in Docker.
10+
911
### ✅ Phase 1: Project Setup
1012
- [x] Copy training documentation to repo
1113
- [x] Create feature branch: `feature/solidity-by-example-dojo`

scripts/slither-summary.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
FILTER='(src/hacks|lib|test|script)(/|$)'
3+
slither . --filter-paths "$FILTER" --json slither-report.json || true
4+
python3 <<'PY'
5+
import json
6+
import os
7+
from collections import Counter
8+
path = "slither-report.json"
9+
if not os.path.exists(path):
10+
print("no report")
11+
raise SystemExit(1)
12+
with open(path) as f:
13+
d = json.load(f)
14+
results = d.get("results", [])
15+
c = Counter(r["check"] for r in results)
16+
print("total_findings", len(results))
17+
for check, n in c.most_common(60):
18+
print(n, check)
19+
PY

scripts/slither_fix_naming.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env python3
2+
"""Apply Slither naming-convention fixes from slither-report.json (dry-run safe: writes files)."""
3+
from __future__ import annotations
4+
5+
import json
6+
import re
7+
import sys
8+
from collections import defaultdict
9+
from pathlib import Path
10+
11+
REPORT = Path("slither-report.json")
12+
ROOT = Path(__file__).resolve().parents[1]
13+
14+
15+
def word_repl(text: str, old: str, new: str) -> str:
16+
pat = re.compile(r"(?<![a-zA-Z0-9_])" + re.escape(old) + r"(?![a-zA-Z0-9_])")
17+
return pat.sub(new, text)
18+
19+
20+
def to_mixed_case_from_caps(name: str) -> str:
21+
"""MY_UINT -> myUint, DOMAIN_SEPARATOR handled elsewhere."""
22+
if "_" in name:
23+
parts = [p for p in name.split("_") if p]
24+
if not parts:
25+
return name.lower()
26+
head = parts[0].lower()
27+
tail = "".join(p.capitalize() for p in parts[1:])
28+
return head + tail
29+
if len(name) == 1:
30+
return name.lower()
31+
return name[0].lower() + name[1:]
32+
33+
34+
def suggest_new_name(name: str, contract: str | None) -> str:
35+
if name.startswith("_"):
36+
return name[1:]
37+
if name == "DOMAIN_SEPARATOR":
38+
return "domainSeparator"
39+
if name == "A" and contract == "StableSwapAMM":
40+
return "amplificationCoefficient"
41+
if name.isupper() or (len(name) > 1 and name[0].isupper() and "_" in name):
42+
return to_mixed_case_from_caps(name)
43+
return name
44+
45+
46+
def main() -> int:
47+
if not REPORT.exists():
48+
print("Missing slither-report.json — run: bash scripts/slither-summary.sh", file=sys.stderr)
49+
return 1
50+
51+
data = json.loads(REPORT.read_text(encoding="utf-8"))
52+
detectors = data["results"]["detectors"]
53+
54+
# (relative_path, start_line, end_line) -> {old: new}
55+
ranges: dict[tuple[str, int, int], dict[str, str]] = defaultdict(dict)
56+
function_renames: dict[tuple[str, str], str] = {} # (file, old_fn) -> new_fn
57+
58+
for det in detectors:
59+
if det["check"] != "naming-convention":
60+
continue
61+
el0 = det["elements"][0]
62+
etype = el0["type"]
63+
64+
if etype == "function":
65+
fname = el0["name"]
66+
rel = el0["source_mapping"]["filename_short"]
67+
new_fn = fname.replace("_UNOPTIMIZED", "Unoptimized")
68+
if new_fn != fname:
69+
function_renames[(rel, fname)] = new_fn
70+
continue
71+
72+
if etype != "variable":
73+
continue
74+
75+
name = el0["name"]
76+
rel = el0["source_mapping"]["filename_short"]
77+
if rel.startswith("src/hacks/"):
78+
continue
79+
80+
parent = el0.get("type_specific_fields", {}).get("parent", {})
81+
contract_name = None
82+
if parent.get("type") == "contract":
83+
contract_name = parent.get("name")
84+
lines = parent["source_mapping"].get("lines", [])
85+
elif parent.get("type") == "function":
86+
gp = parent.get("type_specific_fields", {}).get("parent", {})
87+
if gp.get("type") == "contract":
88+
contract_name = gp.get("name")
89+
lines = parent["source_mapping"].get("lines", [])
90+
else:
91+
lines = []
92+
93+
if not lines:
94+
continue
95+
96+
lo, hi = min(lines), max(lines)
97+
new_name = suggest_new_name(name, contract_name)
98+
if new_name == name:
99+
continue
100+
101+
bucket = ranges[(rel, lo, hi)]
102+
if name in bucket and bucket[name] != new_name:
103+
print(f"Conflict {rel} L{lo}-{hi}: {name} -> {bucket[name]} vs {new_name}", file=sys.stderr)
104+
return 2
105+
bucket[name] = new_name
106+
107+
# Apply function renames file-wise (full file)
108+
by_file_fn: dict[str, dict[str, str]] = defaultdict(dict)
109+
for (rel, old), new in function_renames.items():
110+
by_file_fn[rel][old] = new
111+
112+
for rel, mapping in by_file_fn.items():
113+
path = ROOT / rel
114+
text = path.read_text(encoding="utf-8")
115+
for old, new in sorted(mapping.items(), key=lambda x: len(x[0]), reverse=True):
116+
text = word_repl(text, old, new)
117+
path.write_text(text, encoding="utf-8", newline="\n")
118+
print(f"renamed functions in {rel}: {mapping}")
119+
120+
# Apply variable renames per function range
121+
by_path: dict[str, list[tuple[int, int, dict[str, str]]]] = defaultdict(list)
122+
for (rel, lo, hi), mapping in ranges.items():
123+
if not mapping:
124+
continue
125+
by_path[rel].append((lo, hi, mapping))
126+
127+
for rel, chunks in by_path.items():
128+
path = ROOT / rel
129+
lines = path.read_text(encoding="utf-8").split("\n")
130+
# process smaller inner ranges first if nested? sort by span length ascending
131+
chunks.sort(key=lambda x: (x[1] - x[0], x[0]))
132+
for lo, hi, mapping in chunks:
133+
segment = "\n".join(lines[lo - 1 : hi])
134+
for old, new in sorted(mapping.items(), key=lambda x: len(x[0]), reverse=True):
135+
segment = word_repl(segment, old, new)
136+
new_lines = segment.split("\n")
137+
lines[lo - 1 : hi] = new_lines
138+
path.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
139+
print(f"updated {rel} ({len(chunks)} span(s))")
140+
141+
return 0
142+
143+
144+
if __name__ == "__main__":
145+
raise SystemExit(main())

scripts/slither_naming_stats.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import json
2+
from collections import Counter
3+
4+
with open("slither-report.json", encoding="utf-8") as f:
5+
d = json.load(f)
6+
types = Counter()
7+
for x in d["results"]["detectors"]:
8+
if x["check"] != "naming-convention":
9+
continue
10+
types[x["elements"][0]["type"]] += 1
11+
print(dict(types))

scripts/strip_natspec_params.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python3
2+
"""Remove /// @param lines (avoids Solidity 3881 when names drift after refactors)."""
3+
from __future__ import annotations
4+
5+
import re
6+
from pathlib import Path
7+
8+
ROOT = Path(__file__).resolve().parents[1]
9+
PARAM_LINE = re.compile(r"^\s*///\s*@param\b.*$")
10+
11+
12+
def main() -> int:
13+
removed = 0
14+
for path in sorted(ROOT.glob("src/**/*.sol")):
15+
if "hacks" in path.parts:
16+
continue
17+
text = path.read_text(encoding="utf-8")
18+
lines = text.splitlines()
19+
new_lines = []
20+
for line in lines:
21+
if PARAM_LINE.match(line):
22+
removed += 1
23+
continue
24+
new_lines.append(line)
25+
new_text = "\n".join(new_lines) + ("\n" if text.endswith("\n") else "")
26+
if new_text != text:
27+
path.write_text(new_text, encoding="utf-8", newline="\n")
28+
print("removed_param_lines", removed)
29+
return 0
30+
31+
32+
if __name__ == "__main__":
33+
raise SystemExit(main())

scripts/sync_natspec_params.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env python3
2+
"""Sync NatSpec @param names: strip leading underscore to match mixedCase parameters."""
3+
from __future__ import annotations
4+
5+
import re
6+
import sys
7+
from pathlib import Path
8+
9+
ROOT = Path(__file__).resolve().parents[1]
10+
PARAM_RE = re.compile(r"^(?P<indent>\s*///\s*@param )_(?P<name>[a-zA-Z][a-zA-Z0-9]*)(?P<rest>\s.*)?$")
11+
12+
13+
def main() -> int:
14+
changed = 0
15+
for path in sorted(ROOT.glob("src/**/*.sol")):
16+
if "hacks" in path.parts:
17+
continue
18+
text = path.read_text(encoding="utf-8")
19+
out_lines = []
20+
for line in text.splitlines():
21+
m = PARAM_RE.match(line)
22+
if m:
23+
rest = m.group("rest") or ""
24+
line = f"{m.group('indent')}{m.group('name')}{rest}"
25+
changed += 1
26+
out_lines.append(line)
27+
new_text = "\n".join(out_lines) + ("\n" if text.endswith("\n") else "")
28+
if new_text != text:
29+
path.write_text(new_text, encoding="utf-8", newline="\n")
30+
print("touched_lines", changed)
31+
return 0
32+
33+
34+
if __name__ == "__main__":
35+
raise SystemExit(main())

0 commit comments

Comments
 (0)