Skip to main content

neo_devpack_solidity/
utils.rs

1//! Shared Utility Functions
2//!
3//! Common utilities used across the compiler.
4
5use std::collections::HashMap;
6
7/// Convert a Solidity type string to its canonical form for ABI encoding.
8///
9/// Handles special cases like `struct Foo` and `enum Bar` by extracting
10/// just the type name.
11///
12/// # Examples
13/// ```
14/// use neo_devpack_solidity::utils::canonical_param_type;
15/// assert_eq!(canonical_param_type("uint256"), "uint256");
16/// assert_eq!(canonical_param_type("struct MyStruct"), "MyStruct");
17/// assert_eq!(canonical_param_type("enum MyEnum"), "MyEnum");
18/// ```
19pub fn canonical_param_type(ty: &str) -> String {
20    let mut parts = ty.split_whitespace();
21    match parts.next() {
22        Some("struct" | "enum") => parts.next().unwrap_or_default().to_string(),
23        Some(first) => first.to_string(),
24        None => String::new(),
25    }
26}
27
28/// Simple canonical type extraction (first word only).
29///
30/// Use this when struct/enum special handling is not needed.
31pub fn canonical_param_type_simple(ty: &str) -> String {
32    ty.split_whitespace().next().unwrap_or_default().to_string()
33}
34
35/// Task #106 — Convert a Solidity type string to its EVM-canonical ABI
36/// signature form, expanding struct types into parenthesized tuple
37/// representations.
38///
39/// Unlike `canonical_param_type`, which returns the bare struct NAME, this
40/// version recursively expands struct field types so selectors match the
41/// Ethereum spec:
42///   * `struct P { uint256 a; bool b; }` → `"(uint256,bool)"`
43///   * `struct Outer { P inner; address who; }` → `"((uint256,bool),address)"`
44///
45/// `struct_fields` maps struct NAME to its ordered `(name, ty_string)` list.
46/// Enum types canonicalize to `uint8` per Solidity's `abi.encode` rules.
47/// Unknown user-defined types fall back to the bare name (same as
48/// `canonical_param_type`).
49pub fn canonical_param_type_with_structs(
50    ty: &str,
51    struct_fields: &HashMap<String, Vec<(String, String)>>,
52) -> String {
53    // Strip Solidity data-location keywords that never participate in the
54    // canonical signature.
55    let trimmed = ty.trim();
56    let without_location = trimmed
57        .replace(" memory", "")
58        .replace(" calldata", "")
59        .replace(" storage", "");
60    let t = without_location.trim();
61
62    // Peel a trailing array suffix (`[]` or `[N]`), canonicalize the ELEMENT
63    // type, then re-append the suffix. Without this, an array-of-struct like
64    // `P[]` tokenizes to the whole `"P[]"` (not a struct-map key) and passes
65    // through verbatim, producing a non-conformant selector instead of the
66    // canonical `(uint256,bool)[]`.
67    if t.ends_with(']') {
68        if let Some(open) = t.rfind('[') {
69            let (element, suffix) = t.split_at(open);
70            let inner = canonical_param_type_with_structs(element.trim(), struct_fields);
71            return format!("{inner}{suffix}");
72        }
73    }
74
75    // Handle `struct Name` → expand to `(field_types)`.
76    // Handle `enum Name` → uint8.
77    // Handle bare `Name` that matches a known struct → expand.
78    // Otherwise → passthrough (plain scalar or unknown user-defined type).
79    let mut parts = t.split_whitespace();
80    match parts.next() {
81        Some("struct") => {
82            let name = parts.next().unwrap_or_default();
83            if name.is_empty() {
84                return String::new();
85            }
86            // Guard against infinite recursion on self-referential struct names
87            // by tracking a visited set.
88            let mut visited = std::collections::HashSet::new();
89            expand_struct_canonical(name, struct_fields, &mut visited)
90        }
91        Some("enum") => "uint8".to_string(),
92        Some(first) => {
93            // Bare-name path: solang-parser emits `"P"` (no `struct` prefix)
94            // for struct parameters in some code paths. If `first` names a
95            // known struct in the map, expand it to the tuple form.
96            if struct_fields.contains_key(first) {
97                let mut visited = std::collections::HashSet::new();
98                expand_struct_canonical(first, struct_fields, &mut visited)
99            } else {
100                first.to_string()
101            }
102        }
103        None => String::new(),
104    }
105}
106
107fn expand_struct_canonical(
108    name: &str,
109    struct_fields: &HashMap<String, Vec<(String, String)>>,
110    visited: &mut std::collections::HashSet<String>,
111) -> String {
112    if !visited.insert(name.to_string()) {
113        // Recursive reference — fall back to the bare name to avoid infinite
114        // recursion. Solidity itself forbids recursive value-type structs, but
115        // keep the fallback defensive.
116        return name.to_string();
117    }
118    let Some(fields) = struct_fields.get(name) else {
119        // Unknown struct — fall back to bare name (Task #65 compat).
120        visited.remove(name);
121        return name.to_string();
122    };
123    let field_sigs: Vec<String> = fields
124        .iter()
125        .map(|(_, field_ty)| {
126            // Strip location keywords on fields too.
127            let tt = field_ty
128                .replace(" memory", "")
129                .replace(" calldata", "")
130                .replace(" storage", "");
131            let tt = tt.trim();
132            let mut parts = tt.split_whitespace();
133            match parts.next() {
134                Some("struct") => {
135                    let sub_name = parts.next().unwrap_or_default();
136                    if sub_name.is_empty() {
137                        String::new()
138                    } else {
139                        expand_struct_canonical(sub_name, struct_fields, visited)
140                    }
141                }
142                Some("enum") => "uint8".to_string(),
143                Some(first) => {
144                    if struct_fields.contains_key(first) {
145                        expand_struct_canonical(first, struct_fields, visited)
146                    } else {
147                        first.to_string()
148                    }
149                }
150                None => String::new(),
151            }
152        })
153        .collect();
154    visited.remove(name);
155    format!("({})", field_sigs.join(","))
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_canonical_param_type() {
164        assert_eq!(canonical_param_type("uint256"), "uint256");
165        assert_eq!(canonical_param_type("struct MyStruct"), "MyStruct");
166        assert_eq!(canonical_param_type("enum MyEnum"), "MyEnum");
167        assert_eq!(canonical_param_type("address payable"), "address");
168        assert_eq!(canonical_param_type(""), "");
169    }
170
171    #[test]
172    fn test_canonical_param_type_simple() {
173        assert_eq!(canonical_param_type_simple("uint256"), "uint256");
174        assert_eq!(canonical_param_type_simple("struct MyStruct"), "struct");
175        assert_eq!(canonical_param_type_simple("address payable"), "address");
176    }
177
178    #[test]
179    fn test_canonical_param_type_with_structs_flat() {
180        let mut map: HashMap<String, Vec<(String, String)>> = HashMap::new();
181        map.insert(
182            "P".to_string(),
183            vec![
184                ("a".to_string(), "uint256".to_string()),
185                ("b".to_string(), "bool".to_string()),
186            ],
187        );
188        assert_eq!(
189            canonical_param_type_with_structs("struct P", &map),
190            "(uint256,bool)"
191        );
192        assert_eq!(
193            canonical_param_type_with_structs("struct P memory", &map),
194            "(uint256,bool)"
195        );
196        assert_eq!(
197            canonical_param_type_with_structs("uint256", &map),
198            "uint256"
199        );
200    }
201
202    #[test]
203    fn test_canonical_param_type_with_structs_nested() {
204        let mut map: HashMap<String, Vec<(String, String)>> = HashMap::new();
205        map.insert(
206            "Inner".to_string(),
207            vec![("x".to_string(), "uint256".to_string())],
208        );
209        map.insert(
210            "Outer".to_string(),
211            vec![
212                ("inner".to_string(), "struct Inner".to_string()),
213                ("who".to_string(), "address".to_string()),
214            ],
215        );
216        assert_eq!(
217            canonical_param_type_with_structs("struct Outer", &map),
218            "((uint256),address)"
219        );
220    }
221
222    #[test]
223    fn test_canonical_param_type_with_structs_unknown_fallback() {
224        let map: HashMap<String, Vec<(String, String)>> = HashMap::new();
225        // Unknown struct falls back to bare name.
226        assert_eq!(
227            canonical_param_type_with_structs("struct Unknown", &map),
228            "Unknown"
229        );
230    }
231
232    #[test]
233    fn test_canonical_param_type_with_structs_enum() {
234        let map: HashMap<String, Vec<(String, String)>> = HashMap::new();
235        assert_eq!(
236            canonical_param_type_with_structs("enum MyEnum", &map),
237            "uint8"
238        );
239    }
240}