neo_solidity/solidity/validate/contract/
methods.rs

1fn 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    // Used to reduce false-positive diagnostics for `return foo();` in
14    // multi-return functions. NeoVM lowering represents tuples as arrays, so
15    // returning another tuple-returning function call is valid.
16    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(&param.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) = &param.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            // Validate mapping key types: arrays, structs, and mappings are
108            // not valid as mapping keys because they lack a stable hash on NeoVM.
109            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) = &param.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        // Warn when `payable` is used: Neo N3 does not have native value
152        // transfers, so the modifier is a no-op.  Token receipts should use
153        // the `onNEP17Payment` callback instead.
154        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                // Abstract contracts are allowed to have bodyless functions.
185                // No diagnostic needed here; the abstract contract validation
186                // in entry.rs handles the deployment check.
187            } 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}