1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
fn validate_methods(metadata: &ContractMetadata, diagnostics: &mut Vec<Diagnostic>) -> usize {
use std::collections::{HashMap, HashSet};
let mut signatures = HashSet::new();
let mut exposed_overload_counts: HashSet<(String, usize)> = HashSet::new();
let mut constructor_count = 0usize;
// Struct field map so the duplicate-signature check canonicalizes a struct
// parameter to its ABI TUPLE shape, not its bare name. Two different
// structs with the same shape (`struct A{uint x}` / `struct B{uint y}`)
// produce the same 4-byte selector, so `f(A)` and `f(B)` collide on-chain —
// solc rejects this, but a name-keyed check (`f(A)` != `f(B)`) misses it.
let struct_fields_map: HashMap<String, Vec<(String, String)>> = metadata
.structs
.iter()
.map(|s| {
(
s.name.clone(),
s.fields.iter().map(|f| (f.name.clone(), f.ty.clone())).collect(),
)
})
.collect();
// Used to reduce false-positive diagnostics for `return foo();` in
// multi-return functions. NeoVM lowering represents tuples as arrays, so
// returning another tuple-returning function call is valid.
let mut return_arities: HashMap<(String, usize), usize> = HashMap::new();
for function in &metadata.methods {
return_arities.insert(
(function.name.clone(), function.parameters.len()),
function.return_parameters.len(),
);
}
for function in &metadata.methods {
let is_exposed = matches!(
function.visibility,
VisibilityKind::Public | VisibilityKind::External
);
match function.kind {
FunctionKind::Constructor => {
constructor_count += 1;
if !function.return_parameters.is_empty() {
diagnostics.push(Diagnostic::error("constructor must not specify a return type"));
}
}
FunctionKind::Regular => {
let count_key = (function.name.clone(), function.parameters.len());
if is_exposed && !exposed_overload_counts.insert(count_key.clone()) {
diagnostics.push(
Diagnostic::warning(format!(
"overloaded function '{}' with {} parameter(s) uses Neo overload mangling; \
external callers must invoke the generated Neo method names",
count_key.0, count_key.1
))
.with_code("W130")
.with_suggestion(
"use generated neo_name entries (e.g. functionName(type1,type2)) when invoking this contract from Neo",
),
);
}
let param_signature: Vec<String> = function
.parameters
.iter()
.map(|param| {
crate::utils::canonical_param_type_with_structs(
¶m.ty,
&struct_fields_map,
)
})
.collect();
let signature = format!("{}({})", function.name, param_signature.join(","));
// Internal/private helper methods may be merged from multiple
// libraries for compatibility; only enforce duplicate-signature
// errors on externally visible ABI methods.
if is_exposed && !signatures.insert(signature.clone()) {
diagnostics.push(Diagnostic::error(format!("duplicate function signature '{signature}'")));
}
}
}
let mut params = HashSet::new();
for param in &function.parameters {
if let Some(name) = ¶m.name {
if !params.insert(name.clone()) {
diagnostics.push(Diagnostic::error(format!(
"function '{}' has duplicate parameter name '{}'",
function.name, name
)));
}
}
if param.neo_type.is_none() && is_exposed {
let lower_ty = param.ty.to_ascii_lowercase();
let param_name = param
.name
.clone()
.unwrap_or_else(|| "<unnamed>".to_string());
if lower_ty.starts_with("fixed") || lower_ty.starts_with("ufixed") {
diagnostics.push(
Diagnostic::error(format!(
"function '{}' parameter '{}' uses fixed-point type '{}' which is not supported on NeoVM",
function.name, param_name, param.ty
))
.with_suggestion(
"use scaled integer arithmetic instead (e.g., multiply by 10^18 for 18 decimal places)"
),
);
} else {
diagnostics.push(Diagnostic::error(format!(
"function '{}' parameter '{}' uses unsupported type '{}'",
function.name, param_name, param.ty
)));
}
}
// Validate mapping key types: arrays, structs, and mappings are
// not valid as mapping keys because they lack a stable hash on NeoVM.
if let Some(NeoType::Mapping { ref key, .. }) = param.neo_type {
fn is_invalid_mapping_key(ty: &NeoType) -> bool {
matches!(
ty,
NeoType::Array(..)
| NeoType::Struct { .. }
| NeoType::Mapping { .. }
)
}
if is_invalid_mapping_key(key) {
let param_name = param
.name
.clone()
.unwrap_or_else(|| "<unnamed>".to_string());
diagnostics.push(Diagnostic::error(format!(
"function '{}' parameter '{}': mapping key type must be \
an elementary type (integer, bool, address, string, bytes); \
arrays, structs, and mappings are not allowed as keys",
function.name, param_name
)));
}
}
if let Some(storage) = ¶m.storage {
if storage == "storage"
&& matches!(
function.visibility,
VisibilityKind::External | VisibilityKind::Public
)
{
diagnostics.push(Diagnostic::error(format!(
"public/external function '{}' parameter '{}' may not use 'storage' data location",
function.name,
param
.name
.clone()
.unwrap_or_else(|| "<unnamed>".to_string())
)));
}
}
}
// Warn when `payable` is used: Neo N3 does not have native value
// transfers, so the modifier is a no-op. Token receipts should use
// the `onNEP17Payment` callback instead.
if function.state_mutability == StateMutability::Payable
&& !matches!(function.kind, FunctionKind::Constructor)
{
diagnostics.push(
Diagnostic::warning(format!(
"function '{}' is marked `payable`, but Neo N3 has no native coin \
transfer; the modifier is accepted for compatibility but has no \
effect. Use onNEP17Payment(address, uint256, bytes) to handle \
incoming NEP-17 token payments.",
function.name
))
.with_code("W111")
.with_suggestion(
"Remove `payable` or add an onNEP17Payment callback for token receipts",
),
);
}
if let Some(body) = &function.body {
check_return_statements(
body,
function.return_parameters.len(),
&function.name,
&return_arities,
diagnostics,
);
} else if !function.return_parameters.is_empty()
&& !matches!(function.kind, FunctionKind::Constructor)
{
if metadata.is_abstract || metadata.is_interface {
// Abstract contracts are allowed to have bodyless functions.
// No diagnostic needed here; the abstract contract validation
// in entry.rs handles the deployment check.
} else {
diagnostics.push(
Diagnostic::error(format!(
"function '{}' declares a return type but has no implementation; \
provide a body or mark the contract as 'abstract contract {}'",
function.name, metadata.name
))
.with_suggestion(
"add a function body, or declare the contract as abstract"
),
);
}
}
}
constructor_count
}