neo_solidity/cli/cli_parts/cli_run/
single_file.rs

1fn run_single_file(matches: &clap::ArgMatches) {
2    fn sanitize_stem_or_fallback(stem: Option<&str>, fallback: String) -> String {
3        stem.and_then(standard_json::sanitize_contract_name)
4            .unwrap_or(fallback)
5    }
6
7    fn is_output_directory(path: &str, assume_dir_when_missing: bool) -> bool {
8        fn looks_like_output_file(path: &str) -> bool {
9            path.ends_with(".nef")
10                || path.ends_with(".manifest.json")
11                || path.ends_with(".json")
12                || path.ends_with(".asm")
13        }
14
15        if path.ends_with('/') || path.ends_with('\\') {
16            return true;
17        }
18        let output_path = Path::new(path);
19        if output_path.is_dir() {
20            return true;
21        }
22        assume_dir_when_missing && !looks_like_output_file(path)
23    }
24
25    let sources: Vec<String> = matches
26        .get_many::<String>("source")
27        .map(|vals| vals.map(|s| s.to_string()).collect())
28        .unwrap_or_default();
29
30    if sources.is_empty() {
31        eprintln!("error: no input files provided");
32        std::process::exit(1);
33    }
34
35    let output_arg = matches.get_one::<String>("output").cloned();
36
37    let format = matches
38        .get_one::<String>("format")
39        .map(|s| s.as_str())
40        .unwrap_or("complete");
41    let optimizer_level = matches
42        .get_one::<String>("optimize")
43        .and_then(|s| s.parse::<u8>().ok())
44        .unwrap_or(2)
45        .min(3);
46    let verbose = matches.get_flag("verbose");
47    let nef_source_override = matches.get_one::<String>("nef-source").map(|s| s.as_str());
48    let deployer = matches.get_one::<String>("deployer").map(|s| s.as_str());
49    let include_paths: Vec<std::path::PathBuf> = matches
50        .get_many::<String>("include-path")
51        .map(|vals| vals.map(std::path::PathBuf::from).collect())
52        .unwrap_or_default();
53    let contract_filters: Vec<String> = matches
54        .get_many::<String>("contract")
55        .map(|vals| vals.map(|s| s.to_string()).collect())
56        .unwrap_or_default();
57    let use_callt = matches.get_flag("callt");
58    let deny_wildcard_permissions = matches.get_flag("deny-wildcard-permissions");
59    let deny_wildcard_contracts = matches.get_flag("deny-wildcard-contracts");
60    let deny_wildcard_methods = matches.get_flag("deny-wildcard-methods");
61    let manifest_permissions_file = matches.get_one::<String>("manifest-permissions").map(|s| s.as_str());
62    let manifest_permissions_mode = matches
63        .get_one::<String>("manifest-permissions-mode")
64        .map(|s| s.as_str())
65        .unwrap_or("merge");
66    let json_errors = matches.get_flag("json-errors");
67    let json_warnings = matches.get_flag("json-warnings");
68    let warn_suppress: Vec<String> = matches
69        .get_many::<String>("Wno")
70        .map(|vals| vals.map(|s| s.to_string()).collect())
71        .unwrap_or_default();
72    let warn_promote: Vec<String> = matches
73        .get_many::<String>("Werror")
74        .map(|vals| vals.map(|s| s.to_string()).collect())
75        .unwrap_or_default();
76
77    let manifest_permissions = match manifest_permissions_file {
78        Some(path) => match load_manifest_permissions_override(path, manifest_permissions_mode) {
79            Ok(override_permissions) => Some(override_permissions),
80            Err(err) => {
81                eprintln!("error: {err}");
82                std::process::exit(1);
83            }
84        },
85        None => None,
86    };
87
88    let file_ids: Vec<String> = if sources.len() > 1 {
89        use std::collections::HashMap;
90        use std::path::Component;
91
92        fn file_identity(path: &Path, fallback: String) -> String {
93            let mut parts: Vec<String> = Vec::new();
94            for component in path.components() {
95                match component {
96                    Component::Normal(os) => {
97                        parts.push(os.to_string_lossy().to_string());
98                    }
99                    Component::CurDir | Component::ParentDir => {}
100                    Component::Prefix(_) | Component::RootDir => {}
101                }
102            }
103
104            if parts.is_empty() {
105                return fallback;
106            }
107
108            if let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) {
109                if let Some(last) = parts.last_mut() {
110                    *last = stem.to_string();
111                }
112            }
113
114            let joined = parts.join("_");
115            standard_json::sanitize_contract_name(&joined).unwrap_or(fallback)
116        }
117
118        let mut stem_counts: HashMap<String, usize> = HashMap::new();
119        let stems: Vec<String> = sources
120            .iter()
121            .enumerate()
122            .map(|(file_index, input_file)| {
123                let file_stem = Path::new(input_file)
124                    .file_stem()
125                    .and_then(|stem| stem.to_str());
126                let sanitized =
127                    sanitize_stem_or_fallback(file_stem, format!("contract{file_index}"));
128                *stem_counts.entry(sanitized.clone()).or_insert(0) += 1;
129                sanitized
130            })
131            .collect();
132
133        sources
134            .iter()
135            .enumerate()
136            .map(|(file_index, input_file)| {
137                let stem = stems[file_index].clone();
138                if stem_counts.get(&stem).copied().unwrap_or(0) <= 1 {
139                    stem
140                } else {
141                    file_identity(Path::new(input_file), format!("{stem}-{file_index}"))
142                }
143            })
144            .collect()
145    } else {
146        Vec::new()
147    };
148
149    if verbose {
150        println!("Neo Solidity Compiler v{}", env!("CARGO_PKG_VERSION"));
151        println!("Format: {}", format);
152    }
153
154    let deployer_le = match deployer {
155        Some(value) => match neo_solidity::neo::parse_uint160_hex_be(value) {
156            Ok(parsed) => Some(parsed),
157            Err(err) => {
158                eprintln!("error: invalid --deployer value: {err}");
159                std::process::exit(1);
160            }
161        },
162        None => None,
163    };
164
165    if sources.len() > 1 {
166        if let Some(output) = &output_arg {
167            if !is_output_directory(output, true) {
168                eprintln!(
169                    "error: when compiling multiple input files, --output must be a directory (got '{output}')"
170                );
171                std::process::exit(1);
172            }
173        }
174
175        if verbose {
176            println!("Batch mode: {} input file(s)", sources.len());
177            if let Some(output) = &output_arg {
178                println!("Output directory: {}", output);
179            }
180        }
181    }
182
183    let mut emitted_any = false;
184
185    for (file_index, input_file) in sources.iter().enumerate() {
186        if sources.len() > 1 {
187            println!("(info) compiling {}", input_file);
188        }
189
190        let resolved = match resolve_solidity_sources_with_imports(
191            Path::new(input_file),
192            &include_paths,
193        ) {
194            Ok(resolved) => resolved,
195            Err(err) => {
196                eprintln!("Error resolving imports: {err}");
197                std::process::exit(1);
198            }
199        };
200        let input_content = resolved.combined_source;
201
202        if verbose {
203            println!(
204                "Resolved {} Solidity source file(s) ({} bytes combined)",
205                resolved.files.len(),
206                input_content.len()
207            );
208        }
209
210        let mut artifacts = compile_input_or_exit(
211            &input_content,
212            verbose,
213            CompileOptions {
214                optimizer_level,
215                use_callt,
216                deny_wildcard_permissions,
217                deny_wildcard_contracts,
218                deny_wildcard_methods,
219                manifest_permissions: manifest_permissions.clone(),
220            },
221            json_errors,
222            json_warnings,
223        );
224
225        let had_any_contracts = !artifacts.is_empty();
226
227        if !contract_filters.is_empty() {
228            artifacts.retain(|artifact| {
229                contract_filters
230                    .iter()
231                    .any(|name| name == &artifact.metadata.name)
232            });
233        }
234
235        if artifacts.is_empty() {
236            if !contract_filters.is_empty() && verbose {
237                println!(
238                    "(info) no matching contract(s) found in {} for --contract {}",
239                    input_file,
240                    contract_filters.join(", ")
241                );
242            }
243            if verbose && !had_any_contracts {
244                eprintln!("warning: No contracts were found in {}", input_file);
245            }
246            continue;
247        }
248
249        let output_prefix = match (&output_arg, sources.len() > 1) {
250            (Some(output), true) => {
251                let output_dir = Path::new(output);
252                let stem_sanitized = file_ids
253                    .get(file_index)
254                    .cloned()
255                    .unwrap_or_else(|| format!("contract{file_index}"));
256
257                if artifacts.len() == 1 {
258                    let contract_name = artifacts[0].metadata.name.as_str();
259                    let contract_sanitized =
260                        sanitize_stem_or_fallback(Some(contract_name), stem_sanitized.clone());
261                    let base = if contract_sanitized == stem_sanitized {
262                        stem_sanitized
263                    } else {
264                        format!("{stem_sanitized}-{contract_sanitized}")
265                    };
266                    output_dir.join(base).to_string_lossy().to_string()
267                } else {
268                    output_dir
269                        .join(stem_sanitized)
270                        .to_string_lossy()
271                        .to_string()
272                }
273            }
274            (Some(output), false) => output.clone(),
275            (None, true) => file_ids
276                .get(file_index)
277                .cloned()
278                .unwrap_or_else(|| format!("contract{file_index}")),
279            (None, false) => Path::new(input_file)
280                .file_stem()
281                .and_then(|stem| stem.to_str())
282                .unwrap_or("contract")
283                .to_string(),
284        };
285
286        if verbose {
287            println!("Input: {}", input_file);
288            println!("Output prefix: {}", output_prefix);
289        }
290
291        if artifacts.len() > 1 {
292            println!(
293                "(info) detected {} contracts – outputs are suffixed with their contract names",
294                artifacts.len()
295            );
296        }
297
298        emit_contract_warnings(&artifacts, json_warnings, json_errors, &warn_suppress, &warn_promote);
299        let output_config = OutputConfig {
300            format,
301            output_prefix: &output_prefix,
302            input_file,
303            nef_source_override,
304            deployer: deployer_le,
305            json_errors,
306            json_warnings,
307        };
308        write_contract_outputs(&artifacts, &output_config);
309        emitted_any = true;
310    }
311
312    if !contract_filters.is_empty() && !emitted_any {
313        eprintln!(
314            "error: no matching contract(s) found for --contract {}",
315            contract_filters.join(", ")
316        );
317        std::process::exit(1);
318    }
319
320    println!("🎉 Neo Solidity compilation completed");
321}