neo_solidity/solidity/validate/contract/
methods.rs1fn validate_methods(metadata: &ContractMetadata, diagnostics: &mut Vec<Diagnostic>) -> usize {
2 use std::collections::{HashMap, HashSet};
3
4 fn is_intrinsic_library(name: &str) -> bool {
5 matches!(name, "Runtime" | "Storage" | "Syscalls" | "NativeCalls" | "Neo" | "abi")
6 }
7
8 let mut signatures = HashSet::new();
9 let mut overload_counts: HashSet<(String, usize)> = HashSet::new();
10 let mut overload_has_exposed: HashMap<(String, usize), bool> = HashMap::new();
11 let mut constructor_count = 0usize;
12
13 let mut return_arities: HashMap<(String, usize), usize> = HashMap::new();
17 for function in &metadata.methods {
18 return_arities.insert(
19 (function.name.clone(), function.parameters.len()),
20 function.return_parameters.len(),
21 );
22 }
23
24 for function in &metadata.methods {
25 let is_exposed = matches!(
26 function.visibility,
27 VisibilityKind::Public | VisibilityKind::External
28 );
29
30 match function.kind {
31 FunctionKind::Constructor => {
32 constructor_count += 1;
33 if !function.return_parameters.is_empty() {
34 diagnostics.push(Diagnostic::error("constructor must not specify a return type"));
35 }
36 }
37 FunctionKind::Regular => {
38 let count_key = (function.name.clone(), function.parameters.len());
39 let has_exposed = overload_has_exposed
40 .get(&count_key)
41 .copied()
42 .unwrap_or(false);
43
44 let intrinsic_internal_overload = is_intrinsic_library(&metadata.name)
45 && !is_exposed
46 && !has_exposed;
47
48 if !overload_counts.insert(count_key.clone()) && !intrinsic_internal_overload {
49 diagnostics.push(Diagnostic::error(format!(
50 "overloaded function '{}' with {} parameter(s) is not supported; \
51 Neo ABI dispatches by name and argument count only, so overloads \
52 that differ only in parameter types cannot be distinguished at runtime",
53 count_key.0, count_key.1
54 )));
55 }
56
57 overload_has_exposed.insert(count_key.clone(), has_exposed || is_exposed);
58
59 let param_signature: Vec<String> = function
60 .parameters
61 .iter()
62 .map(|param| canonical_param_type(¶m.ty))
63 .collect();
64 let signature = format!("{}({})", function.name, param_signature.join(","));
65
66 if !signatures.insert(signature.clone()) {
67 diagnostics.push(Diagnostic::error(format!("duplicate function signature '{}'", signature)));
68 }
69 }
70 }
71
72 let mut params = HashSet::new();
73 for param in &function.parameters {
74 if let Some(name) = ¶m.name {
75 if !params.insert(name.clone()) {
76 diagnostics.push(Diagnostic::error(format!(
77 "function '{}' has duplicate parameter name '{}'",
78 function.name, name
79 )));
80 }
81 }
82
83 if param.neo_type.is_none() && is_exposed {
84 let lower_ty = param.ty.to_ascii_lowercase();
85 let param_name = param
86 .name
87 .clone()
88 .unwrap_or_else(|| "<unnamed>".to_string());
89 if lower_ty.starts_with("fixed") || lower_ty.starts_with("ufixed") {
90 diagnostics.push(
91 Diagnostic::error(format!(
92 "function '{}' parameter '{}' uses fixed-point type '{}' which is not supported on NeoVM",
93 function.name, param_name, param.ty
94 ))
95 .with_suggestion(
96 "use scaled integer arithmetic instead (e.g., multiply by 10^18 for 18 decimal places)"
97 ),
98 );
99 } else {
100 diagnostics.push(Diagnostic::error(format!(
101 "function '{}' parameter '{}' uses unsupported type '{}'",
102 function.name, param_name, param.ty
103 )));
104 }
105 }
106
107 if let Some(NeoType::Mapping { ref key, .. }) = param.neo_type {
110 fn is_invalid_mapping_key(ty: &NeoType) -> bool {
111 matches!(
112 ty,
113 NeoType::Array(_)
114 | NeoType::Struct { .. }
115 | NeoType::Mapping { .. }
116 )
117 }
118 if is_invalid_mapping_key(key) {
119 let param_name = param
120 .name
121 .clone()
122 .unwrap_or_else(|| "<unnamed>".to_string());
123 diagnostics.push(Diagnostic::error(format!(
124 "function '{}' parameter '{}': mapping key type must be \
125 an elementary type (integer, bool, address, string, bytes); \
126 arrays, structs, and mappings are not allowed as keys",
127 function.name, param_name
128 )));
129 }
130 }
131
132 if let Some(storage) = ¶m.storage {
133 if storage == "storage"
134 && matches!(
135 function.visibility,
136 VisibilityKind::External | VisibilityKind::Public
137 )
138 {
139 diagnostics.push(Diagnostic::error(format!(
140 "public/external function '{}' parameter '{}' may not use 'storage' data location",
141 function.name,
142 param
143 .name
144 .clone()
145 .unwrap_or_else(|| "<unnamed>".to_string())
146 )));
147 }
148 }
149 }
150
151 if function.state_mutability == StateMutability::Payable
155 && !matches!(function.kind, FunctionKind::Constructor)
156 {
157 diagnostics.push(
158 Diagnostic::warning(format!(
159 "function '{}' is marked `payable`, but Neo N3 has no native coin \
160 transfer; the modifier is accepted for compatibility but has no \
161 effect. Use onNEP17Payment(address, uint256, bytes) to handle \
162 incoming NEP-17 token payments.",
163 function.name
164 ))
165 .with_code("W111")
166 .with_suggestion(
167 "Remove `payable` or add an onNEP17Payment callback for token receipts",
168 ),
169 );
170 }
171
172 if let Some(body) = &function.body {
173 check_return_statements(
174 body,
175 function.return_parameters.len(),
176 &function.name,
177 &return_arities,
178 diagnostics,
179 );
180 } else if !function.return_parameters.is_empty()
181 && !matches!(function.kind, FunctionKind::Constructor)
182 {
183 if metadata.is_abstract {
184 } else {
188 diagnostics.push(
189 Diagnostic::error(format!(
190 "function '{}' declares a return type but has no implementation; \
191 provide a body or mark the contract as 'abstract contract {}'",
192 function.name, metadata.name
193 ))
194 .with_suggestion(
195 "add a function body, or declare the contract as abstract"
196 ),
197 );
198 }
199 }
200 }
201
202 constructor_count
203}