Skip to content

Commit 0067daa

Browse files
committed
Track natspec of state variables
1 parent c4a2480 commit 0067daa

File tree

2 files changed

+183
-11
lines changed

2 files changed

+183
-11
lines changed

crytic_compile/utils/natspec.py

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,70 @@
33
"""
44

55

6+
class DevStateVariable:
7+
"""
8+
Model the dev state variable
9+
"""
10+
11+
def __init__(self, variable: dict) -> None:
12+
"""Init the object
13+
14+
Args:
15+
method (Dict): Method infos (details, params, returns, custom:*)
16+
"""
17+
self._details: str | None = variable.get("details", None)
18+
if "returns" in variable:
19+
self._returns: dict[str, str] = variable["returns"]
20+
elif "return" in variable:
21+
self._returns: dict[str, str] = {"_0": variable["return"]}
22+
else:
23+
self._returns: dict[str, str] = {}
24+
# Extract custom fields (keys starting with "custom:")
25+
self._custom: dict[str, str] = {
26+
k: v for k, v in variable.items() if k.startswith("custom:")
27+
}
28+
29+
@property
30+
def details(self) -> str | None:
31+
"""Return the state variable details
32+
33+
Returns:
34+
Optional[str]: state variable details
35+
"""
36+
return self._details
37+
38+
@property
39+
def variable_returns(self) -> dict[str, str]:
40+
"""Return the state variable returns
41+
42+
Returns:
43+
dict[str, str]: state variable returns
44+
"""
45+
return self._returns
46+
47+
@property
48+
def custom(self) -> dict[str, str]:
49+
"""Return the state variable custom fields
50+
51+
Returns:
52+
Dict[str, str]: custom field name => value (e.g. "custom:security" => "value")
53+
"""
54+
return self._custom
55+
56+
def export(self) -> dict:
57+
"""Export to a python dict
58+
59+
Returns:
60+
Dict: Exported dev state variable
61+
"""
62+
result = {
63+
"details": self.details,
64+
"returns": self.variable_returns,
65+
"custom": self.custom,
66+
}
67+
return result
68+
69+
670
class UserMethod:
771
"""
872
Model the user method
@@ -47,12 +111,17 @@ def __init__(self, method: dict) -> None:
47111
"""Init the object
48112
49113
Args:
50-
method (Dict): Method infos (author, details, params, return, custom:*)
114+
method (Dict): Method infos (author, details, params, returns, custom:*)
51115
"""
52116
self._author: str | None = method.get("author", None)
53117
self._details: str | None = method.get("details", None)
54118
self._params: dict[str, str] = method.get("params", {})
55-
self._return: str | None = method.get("return", None)
119+
if "returns" in method:
120+
self._returns: dict[str, str] = method["returns"]
121+
elif "return" in method:
122+
self._returns: dict[str, str] = {"_0": method["return"]}
123+
else:
124+
self._returns: dict[str, str] = {}
56125
# Extract custom fields (keys starting with "custom:")
57126
self._custom: dict[str, str] = {k: v for k, v in method.items() if k.startswith("custom:")}
58127

@@ -75,13 +144,13 @@ def details(self) -> str | None:
75144
return self._details
76145

77146
@property
78-
def method_return(self) -> str | None:
79-
"""Return the method return
147+
def method_returns(self) -> dict[str, str]:
148+
"""Return the method returns
80149
81150
Returns:
82-
Optional[str]: method return
151+
dict[str, str]: method returns
83152
"""
84-
return self._return
153+
return self._returns
85154

86155
@property
87156
def params(self) -> dict[str, str]:
@@ -111,7 +180,7 @@ def export(self) -> dict:
111180
"author": self.author,
112181
"details": self.details,
113182
"params": self.params,
114-
"return": self.method_return,
183+
"returns": self.method_returns,
115184
}
116185
# Include custom fields if present
117186
result.update(self.custom)
@@ -180,6 +249,9 @@ def __init__(self, devdoc: dict):
180249
self._methods: dict[str, DevMethod] = {
181250
k: DevMethod(item) for k, item in devdoc.get("methods", {}).items()
182251
}
252+
self._state_variables: dict[str, DevStateVariable] = {
253+
k: DevStateVariable(item) for k, item in devdoc.get("stateVariables", {}).items()
254+
}
183255
self._title: str | None = devdoc.get("title", None)
184256
# Extract contract-level custom fields (keys starting with "custom:")
185257
self._custom: dict[str, str] = {k: v for k, v in devdoc.items() if k.startswith("custom:")}
@@ -211,6 +283,15 @@ def methods(self) -> dict[str, DevMethod]:
211283
"""
212284
return self._methods
213285

286+
@property
287+
def state_variables(self) -> dict[str, DevStateVariable]:
288+
"""Return the dev state variables
289+
290+
Returns:
291+
Dict[str, DevStateVariable]: state_variable_name => DevStateVariable
292+
"""
293+
return self._state_variables
294+
214295
@property
215296
def title(self) -> str | None:
216297
"""Return the dev title
@@ -240,6 +321,7 @@ def export(self) -> dict:
240321
"author": self.author,
241322
"details": self.details,
242323
"title": self.title,
324+
"state_variables": self.state_variables,
243325
}
244326
# Include custom fields if present
245327
result.update(self.custom)

tests/test_natspec.py

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22
Test NatSpec parsing, including custom fields (@custom:*)
33
"""
44

5-
from crytic_compile.utils.natspec import DevDoc, DevMethod, Natspec, UserDoc, UserMethod
5+
from crytic_compile.utils.natspec import (
6+
DevDoc,
7+
DevMethod,
8+
DevStateVariable,
9+
Natspec,
10+
UserDoc,
11+
UserMethod,
12+
)
613

714

815
class TestUserMethod:
@@ -40,7 +47,7 @@ def test_devmethod_basic_fields(self) -> None:
4047
assert method.author == "Test Author"
4148
assert method.details == "Method details"
4249
assert method.params == {"a": "first param", "b": "second param"}
43-
assert method.method_return == "return value description"
50+
assert method.method_returns == {"_0": "return value description"}
4451

4552
def test_devmethod_custom_fields_parsing(self) -> None:
4653
"""Test DevMethod extracts custom fields"""
@@ -80,7 +87,7 @@ def test_devmethod_export_includes_custom(self) -> None:
8087
assert exported["author"] == "Test Author"
8188
assert exported["details"] == "Details"
8289
assert exported["params"] == {"x": "param x"}
83-
assert exported["return"] == "returns something"
90+
assert exported["returns"] == {"_0": "returns something"}
8491
assert exported["custom:security"] == "critical"
8592
assert exported["custom:audit"] == "passed"
8693

@@ -90,9 +97,92 @@ def test_devmethod_empty_method(self) -> None:
9097
assert method.author is None
9198
assert method.details is None
9299
assert method.params == {}
93-
assert method.method_return is None
100+
assert method.method_returns == {}
94101
assert method.custom == {}
95102

103+
def test_devmethod_returns_dict(self) -> None:
104+
"""Test DevMethod with 'returns' dict field (multiple return values)"""
105+
method_data = {
106+
"details": "Method with multiple returns",
107+
"returns": {"_0": "first value", "_1": "second value"},
108+
}
109+
method = DevMethod(method_data)
110+
assert method.method_returns == {"_0": "first value", "_1": "second value"}
111+
112+
def test_devmethod_returns_takes_precedence(self) -> None:
113+
"""Test DevMethod prefers 'returns' over 'return' when both present"""
114+
method_data = {
115+
"returns": {"_0": "from returns"},
116+
"return": "from return",
117+
}
118+
method = DevMethod(method_data)
119+
assert method.method_returns == {"_0": "from returns"}
120+
121+
122+
class TestDevStateVariable:
123+
"""Tests for DevStateVariable class"""
124+
125+
def test_state_variable_with_returns_dict(self) -> None:
126+
"""Test DevStateVariable with 'returns' dict field"""
127+
var_data = {
128+
"details": "A state variable",
129+
"returns": {"_0": "the stored value"},
130+
}
131+
var = DevStateVariable(var_data)
132+
assert var.details == "A state variable"
133+
assert var.variable_returns == {"_0": "the stored value"}
134+
135+
def test_state_variable_with_return_string(self) -> None:
136+
"""Test DevStateVariable falls back to 'return' string field"""
137+
var_data = {
138+
"details": "A state variable",
139+
"return": "the stored value",
140+
}
141+
var = DevStateVariable(var_data)
142+
assert var.variable_returns == {"_0": "the stored value"}
143+
144+
def test_state_variable_returns_takes_precedence(self) -> None:
145+
"""Test DevStateVariable prefers 'returns' over 'return' when both present"""
146+
var_data = {
147+
"returns": {"_0": "from returns"},
148+
"return": "from return",
149+
}
150+
var = DevStateVariable(var_data)
151+
assert var.variable_returns == {"_0": "from returns"}
152+
153+
def test_state_variable_empty(self) -> None:
154+
"""Test DevStateVariable with empty dict"""
155+
var = DevStateVariable({})
156+
assert var.details is None
157+
assert var.variable_returns == {}
158+
assert var.custom == {}
159+
160+
def test_state_variable_custom_fields(self) -> None:
161+
"""Test DevStateVariable extracts custom fields"""
162+
var_data = {
163+
"details": "A variable",
164+
"custom:security": "sensitive",
165+
"custom:deprecated": "true",
166+
}
167+
var = DevStateVariable(var_data)
168+
assert var.custom == {
169+
"custom:security": "sensitive",
170+
"custom:deprecated": "true",
171+
}
172+
173+
def test_state_variable_export(self) -> None:
174+
"""Test DevStateVariable export"""
175+
var_data = {
176+
"details": "A state variable",
177+
"returns": {"_0": "the value"},
178+
"custom:audit": "verified",
179+
}
180+
var = DevStateVariable(var_data)
181+
exported = var.export()
182+
assert exported["details"] == "A state variable"
183+
assert exported["returns"] == {"_0": "the value"}
184+
assert exported["custom"] == {"custom:audit": "verified"}
185+
96186

97187
class TestUserDoc:
98188
"""Tests for UserDoc class"""

0 commit comments

Comments
 (0)