1use crate::config::FoundryConfig;
8use crate::runner::RunnerError;
9use serde_json::{Map, Value, json};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::sync::{Mutex, OnceLock};
13use tokio::process::Command;
14use tower_lsp::lsp_types::Url;
15
16static INSTALLED_VERSIONS: OnceLock<Mutex<Vec<SemVer>>> = OnceLock::new();
19
20fn get_installed_versions() -> Vec<SemVer> {
21 let mutex = INSTALLED_VERSIONS.get_or_init(|| Mutex::new(scan_installed_versions()));
22 mutex.lock().unwrap().clone()
23}
24
25fn invalidate_installed_versions() {
26 if let Some(mutex) = INSTALLED_VERSIONS.get() {
27 *mutex.lock().unwrap() = scan_installed_versions();
28 }
29}
30
31fn semver_to_local(v: &semver::Version) -> SemVer {
33 SemVer {
34 major: v.major as u32,
35 minor: v.minor as u32,
36 patch: v.patch as u32,
37 }
38}
39
40pub async fn resolve_solc_binary(
53 config: &FoundryConfig,
54 file_source: Option<&str>,
55 client: Option<&tower_lsp::Client>,
56) -> PathBuf {
57 if let Some(source) = file_source
59 && let Some(constraint) = parse_pragma(source)
60 {
61 if !matches!(constraint, PragmaConstraint::Exact(_))
67 && let Some(ref config_ver) = config.solc_version
68 && let Some(parsed) = SemVer::parse(config_ver)
69 && version_satisfies(&parsed, &constraint)
70 && let Some(path) = find_solc_binary(config_ver)
71 {
72 if let Some(c) = client {
73 c.log_message(
74 tower_lsp::lsp_types::MessageType::INFO,
75 format!(
76 "solc: foundry.toml {config_ver} satisfies pragma {constraint:?} → {}",
77 path.display()
78 ),
79 )
80 .await;
81 }
82 return path;
83 }
84
85 let installed = get_installed_versions();
86 if let Some(version) = find_matching_version(&constraint, &installed)
87 && let Some(path) = find_solc_binary(&version.to_string())
88 {
89 if let Some(c) = client {
90 c.log_message(
91 tower_lsp::lsp_types::MessageType::INFO,
92 format!(
93 "solc: pragma {constraint:?} → {version} → {}",
94 path.display()
95 ),
96 )
97 .await;
98 }
99 return path;
100 }
101
102 let install_version = version_to_install(&constraint);
104 if let Some(ref ver_str) = install_version {
105 if let Some(c) = client {
106 c.show_message(
107 tower_lsp::lsp_types::MessageType::INFO,
108 format!("Installing solc {ver_str}..."),
109 )
110 .await;
111 }
112
113 if svm_install(ver_str).await {
114 invalidate_installed_versions();
116
117 if let Some(c) = client {
118 c.show_message(
119 tower_lsp::lsp_types::MessageType::INFO,
120 format!("Installed solc {ver_str}"),
121 )
122 .await;
123 }
124 if let Some(path) = find_solc_binary(ver_str) {
125 return path;
126 }
127 } else if let Some(c) = client {
128 c.show_message(
129 tower_lsp::lsp_types::MessageType::WARNING,
130 format!(
131 "Failed to install solc {ver_str}. \
132 Install it manually: svm install {ver_str}"
133 ),
134 )
135 .await;
136 }
137 }
138 }
139
140 if let Some(ref version) = config.solc_version
142 && let Some(path) = find_solc_binary(version)
143 {
144 if let Some(c) = client {
145 c.log_message(
146 tower_lsp::lsp_types::MessageType::INFO,
147 format!(
148 "solc: no pragma, using foundry.toml version {version} → {}",
149 path.display()
150 ),
151 )
152 .await;
153 }
154 return path;
155 }
156
157 if let Some(c) = client {
159 c.log_message(
160 tower_lsp::lsp_types::MessageType::INFO,
161 "solc: no pragma match, falling back to system solc",
162 )
163 .await;
164 }
165 PathBuf::from("solc")
166}
167
168fn version_to_install(constraint: &PragmaConstraint) -> Option<String> {
175 match constraint {
176 PragmaConstraint::Exact(v) => Some(v.to_string()),
177 PragmaConstraint::Caret(v) => Some(v.to_string()),
178 PragmaConstraint::Gte(v) => Some(v.to_string()),
179 PragmaConstraint::Range(lower, _) => Some(lower.to_string()),
180 }
181}
182
183async fn svm_install(version: &str) -> bool {
187 let ver = match semver::Version::parse(version) {
188 Ok(v) => v,
189 Err(_) => return false,
190 };
191 svm::install(&ver).await.is_ok()
192}
193
194fn find_solc_binary(version: &str) -> Option<PathBuf> {
196 let path = svm::version_binary(version);
197 if path.is_file() {
198 return Some(path);
199 }
200 None
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
207pub struct SemVer {
208 pub major: u32,
209 pub minor: u32,
210 pub patch: u32,
211}
212
213impl SemVer {
214 fn parse(s: &str) -> Option<SemVer> {
215 let parts: Vec<&str> = s.split('.').collect();
216 if parts.len() != 3 {
217 return None;
218 }
219 Some(SemVer {
220 major: parts[0].parse().ok()?,
221 minor: parts[1].parse().ok()?,
222 patch: parts[2].parse().ok()?,
223 })
224 }
225}
226
227impl std::fmt::Display for SemVer {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
230 }
231}
232
233#[derive(Debug, Clone, PartialEq)]
235pub enum PragmaConstraint {
236 Exact(SemVer),
238 Caret(SemVer),
241 Gte(SemVer),
243 Range(SemVer, SemVer),
245}
246
247pub fn parse_pragma(source: &str) -> Option<PragmaConstraint> {
255 let pragma_line = source
257 .lines()
258 .take(20)
259 .find(|line| line.trim_start().starts_with("pragma solidity"))?;
260
261 let after_keyword = pragma_line
263 .trim_start()
264 .strip_prefix("pragma solidity")?
265 .trim();
266 let constraint_str = after_keyword
267 .strip_suffix(';')
268 .unwrap_or(after_keyword)
269 .trim();
270
271 if constraint_str.is_empty() {
272 return None;
273 }
274
275 if let Some(rest) = constraint_str.strip_prefix(">=") {
277 let rest = rest.trim();
278 if let Some(space_idx) = rest.find(|c: char| c.is_whitespace() || c == '<') {
279 let lower_str = rest[..space_idx].trim();
280 let upper_part = rest[space_idx..].trim();
281 if let Some(upper_str) = upper_part.strip_prefix('<') {
282 let upper_str = upper_str.trim();
283 if let (Some(lower), Some(upper)) =
284 (SemVer::parse(lower_str), SemVer::parse(upper_str))
285 {
286 return Some(PragmaConstraint::Range(lower, upper));
287 }
288 }
289 }
290 if let Some(ver) = SemVer::parse(rest) {
292 return Some(PragmaConstraint::Gte(ver));
293 }
294 }
295
296 if let Some(rest) = constraint_str.strip_prefix('^')
298 && let Some(ver) = SemVer::parse(rest.trim())
299 {
300 return Some(PragmaConstraint::Caret(ver));
301 }
302
303 if let Some(ver) = SemVer::parse(constraint_str) {
305 return Some(PragmaConstraint::Exact(ver));
306 }
307
308 None
309}
310
311pub fn list_installed_versions() -> Vec<SemVer> {
313 get_installed_versions()
314}
315
316fn scan_installed_versions() -> Vec<SemVer> {
320 svm::installed_versions()
321 .unwrap_or_default()
322 .iter()
323 .map(semver_to_local)
324 .collect()
325}
326
327pub fn find_matching_version(
332 constraint: &PragmaConstraint,
333 installed: &[SemVer],
334) -> Option<SemVer> {
335 let candidates: Vec<&SemVer> = installed
336 .iter()
337 .filter(|v| version_satisfies(v, constraint))
338 .collect();
339
340 candidates.last().cloned().cloned()
342}
343
344pub fn version_satisfies(version: &SemVer, constraint: &PragmaConstraint) -> bool {
346 match constraint {
347 PragmaConstraint::Exact(v) => version == v,
348 PragmaConstraint::Caret(v) => {
349 version.major == v.major && version >= v && version.minor < v.minor + 1
352 }
353 PragmaConstraint::Gte(v) => version >= v,
354 PragmaConstraint::Range(lower, upper) => version >= lower && version < upper,
355 }
356}
357
358pub async fn resolve_remappings(config: &FoundryConfig) -> Vec<String> {
362 let output = Command::new("forge")
365 .arg("remappings")
366 .current_dir(&config.root)
367 .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
368 .output()
369 .await;
370
371 if let Ok(output) = output
372 && output.status.success()
373 {
374 let stdout = String::from_utf8_lossy(&output.stdout);
375 let remappings: Vec<String> = stdout
376 .lines()
377 .filter(|l| !l.trim().is_empty())
378 .map(|l| l.to_string())
379 .collect();
380 if !remappings.is_empty() {
381 return remappings;
382 }
383 }
384
385 if !config.remappings.is_empty() {
387 return config.remappings.clone();
388 }
389
390 let remappings_txt = config.root.join("remappings.txt");
392 if let Ok(content) = std::fs::read_to_string(&remappings_txt) {
393 return content
394 .lines()
395 .filter(|l| !l.trim().is_empty())
396 .map(|l| l.to_string())
397 .collect();
398 }
399
400 Vec::new()
401}
402
403pub fn build_standard_json_input(
420 file_path: &str,
421 remappings: &[String],
422 config: &FoundryConfig,
423) -> Value {
424 let mut contract_outputs = vec!["abi", "devdoc", "userdoc", "evm.methodIdentifiers"];
427 if !config.via_ir {
428 contract_outputs.push("evm.gasEstimates");
429 }
430
431 let mut settings = json!({
432 "remappings": remappings,
433 "outputSelection": {
434 "*": {
435 "*": contract_outputs,
436 "": ["ast"]
437 }
438 }
439 });
440
441 if config.via_ir {
442 settings["viaIR"] = json!(true);
443 }
444
445 if let Some(ref evm_version) = config.evm_version {
447 settings["evmVersion"] = json!(evm_version);
448 }
449
450 json!({
451 "language": "Solidity",
452 "sources": {
453 file_path: {
454 "urls": [file_path]
455 }
456 },
457 "settings": settings
458 })
459}
460
461pub async fn run_solc(
463 solc_binary: &Path,
464 input: &Value,
465 project_root: &Path,
466) -> Result<Value, RunnerError> {
467 let input_str = serde_json::to_string(input)?;
468
469 let mut child = Command::new(solc_binary)
470 .arg("--standard-json")
471 .current_dir(project_root)
472 .stdin(std::process::Stdio::piped())
473 .stdout(std::process::Stdio::piped())
474 .stderr(std::process::Stdio::piped())
475 .spawn()?;
476
477 if let Some(mut stdin) = child.stdin.take() {
479 use tokio::io::AsyncWriteExt;
480 stdin
481 .write_all(input_str.as_bytes())
482 .await
483 .map_err(RunnerError::CommandError)?;
484 }
486
487 let output = child
488 .wait_with_output()
489 .await
490 .map_err(RunnerError::CommandError)?;
491
492 let stdout = String::from_utf8_lossy(&output.stdout);
494 if stdout.trim().is_empty() {
495 let stderr = String::from_utf8_lossy(&output.stderr);
496 return Err(RunnerError::CommandError(std::io::Error::other(format!(
497 "solc produced no output, stderr: {stderr}"
498 ))));
499 }
500
501 let parsed: Value = serde_json::from_str(&stdout)?;
502 Ok(parsed)
503}
504
505pub fn normalize_solc_output(mut solc_output: Value, project_root: Option<&Path>) -> Value {
526 fn resolve_import_absolute_paths(node: &mut Value, resolve: &dyn Fn(&str) -> String) {
528 let is_import = node.get("nodeType").and_then(|v| v.as_str()) == Some("ImportDirective");
529
530 if is_import {
531 if let Some(abs_path) = node.get("absolutePath").and_then(|v| v.as_str()) {
532 let resolved = resolve(abs_path);
533 node.as_object_mut()
534 .unwrap()
535 .insert("absolutePath".to_string(), json!(resolved));
536 }
537 }
538
539 if let Some(nodes) = node.get_mut("nodes").and_then(|v| v.as_array_mut()) {
541 for child in nodes {
542 resolve_import_absolute_paths(child, resolve);
543 }
544 }
545 }
546 let mut result = Map::new();
547
548 let errors = solc_output
550 .get_mut("errors")
551 .map(Value::take)
552 .unwrap_or_else(|| json!([]));
553 result.insert("errors".to_string(), errors);
554
555 let resolve = |p: &str| -> String {
558 if let Some(root) = project_root {
559 let path = Path::new(p);
560 if path.is_relative() {
561 return root.join(path).to_string_lossy().into_owned();
562 }
563 }
564 p.to_string()
565 };
566
567 let mut source_id_to_path = Map::new();
570 let mut resolved_sources = Map::new();
571
572 if let Some(sources) = solc_output
573 .get_mut("sources")
574 .and_then(|s| s.as_object_mut())
575 {
576 let keys: Vec<String> = sources.keys().cloned().collect();
578 for key in keys {
579 if let Some(mut source_data) = sources.remove(&key) {
580 let abs_key = resolve(&key);
581
582 if let Some(ast) = source_data.get_mut("ast") {
586 if let Some(abs_path) = ast.get("absolutePath").and_then(|v| v.as_str()) {
587 let resolved = resolve(abs_path);
588 ast.as_object_mut()
589 .unwrap()
590 .insert("absolutePath".to_string(), json!(resolved));
591 }
592 resolve_import_absolute_paths(ast, &resolve);
593 }
594
595 if let Some(id) = source_data.get("id") {
596 source_id_to_path.insert(id.to_string(), json!(&abs_key));
597 }
598
599 resolved_sources.insert(abs_key, source_data);
600 }
601 }
602 }
603
604 result.insert("sources".to_string(), Value::Object(resolved_sources));
605
606 let mut resolved_contracts = Map::new();
608 if let Some(contracts) = solc_output
609 .get_mut("contracts")
610 .and_then(|c| c.as_object_mut())
611 {
612 let keys: Vec<String> = contracts.keys().cloned().collect();
613 for key in keys {
614 if let Some(contract_data) = contracts.remove(&key) {
615 resolved_contracts.insert(resolve(&key), contract_data);
616 }
617 }
618 }
619 result.insert("contracts".to_string(), Value::Object(resolved_contracts));
620
621 result.insert(
623 "source_id_to_path".to_string(),
624 Value::Object(source_id_to_path),
625 );
626
627 Value::Object(result)
628}
629
630pub fn normalize_forge_output(mut forge_output: Value) -> Value {
642 let mut result = Map::new();
643
644 let errors = forge_output
646 .get_mut("errors")
647 .map(Value::take)
648 .unwrap_or_else(|| json!([]));
649 result.insert("errors".to_string(), errors);
650
651 let mut normalized_sources = Map::new();
653 if let Some(sources) = forge_output
654 .get_mut("sources")
655 .and_then(|s| s.as_object_mut())
656 {
657 for (path, entries) in sources.iter_mut() {
658 if let Some(arr) = entries.as_array_mut()
659 && let Some(first) = arr.first_mut()
660 && let Some(sf) = first.get_mut("source_file")
661 {
662 normalized_sources.insert(path.clone(), sf.take());
663 }
664 }
665 }
666 result.insert("sources".to_string(), Value::Object(normalized_sources));
667
668 let mut normalized_contracts = Map::new();
670 if let Some(contracts) = forge_output
671 .get_mut("contracts")
672 .and_then(|c| c.as_object_mut())
673 {
674 for (path, names) in contracts.iter_mut() {
675 let mut path_contracts = Map::new();
676 if let Some(names_obj) = names.as_object_mut() {
677 for (name, entries) in names_obj.iter_mut() {
678 if let Some(arr) = entries.as_array_mut()
679 && let Some(first) = arr.first_mut()
680 && let Some(contract) = first.get_mut("contract")
681 {
682 path_contracts.insert(name.clone(), contract.take());
683 }
684 }
685 }
686 normalized_contracts.insert(path.clone(), Value::Object(path_contracts));
687 }
688 }
689 result.insert("contracts".to_string(), Value::Object(normalized_contracts));
690
691 let source_id_to_path = forge_output
693 .get_mut("build_infos")
694 .and_then(|bi| bi.as_array_mut())
695 .and_then(|arr| arr.first_mut())
696 .and_then(|info| info.get_mut("source_id_to_path"))
697 .map(Value::take)
698 .unwrap_or_else(|| json!({}));
699 result.insert("source_id_to_path".to_string(), source_id_to_path);
700
701 Value::Object(result)
702}
703
704pub async fn solc_ast(
709 file_path: &str,
710 config: &FoundryConfig,
711 client: Option<&tower_lsp::Client>,
712) -> Result<Value, RunnerError> {
713 let file_source = std::fs::read_to_string(file_path).ok();
715 let solc_binary = resolve_solc_binary(config, file_source.as_deref(), client).await;
716 let remappings = resolve_remappings(config).await;
717
718 let rel_path = Path::new(file_path)
723 .strip_prefix(&config.root)
724 .map(|p| p.to_string_lossy().into_owned())
725 .unwrap_or_else(|_| file_path.to_string());
726
727 let input = build_standard_json_input(&rel_path, &remappings, config);
728 let raw_output = run_solc(&solc_binary, &input, &config.root).await?;
729
730 Ok(normalize_solc_output(raw_output, Some(&config.root)))
731}
732
733pub async fn solc_build(
735 file_path: &str,
736 config: &FoundryConfig,
737 client: Option<&tower_lsp::Client>,
738) -> Result<Value, RunnerError> {
739 solc_ast(file_path, config, client).await
740}
741
742const ALWAYS_SKIP_DIRS: &[&str] = &["node_modules", "out", "artifacts", "cache"];
747
748pub fn discover_source_files(config: &FoundryConfig) -> Vec<PathBuf> {
759 let root = &config.root;
760 if !root.is_dir() {
761 return Vec::new();
762 }
763 let mut files = Vec::new();
764 discover_recursive(root, &config.libs, &mut files);
765 files.sort();
766 files
767}
768
769fn discover_recursive(dir: &Path, libs: &[String], files: &mut Vec<PathBuf>) {
770 let entries = match std::fs::read_dir(dir) {
771 Ok(e) => e,
772 Err(_) => return,
773 };
774 for entry in entries.flatten() {
775 let path = entry.path();
776 if path.is_dir() {
777 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
778 if name.starts_with('.') {
780 continue;
781 }
782 if ALWAYS_SKIP_DIRS.contains(&name) {
784 continue;
785 }
786 if libs.iter().any(|lib| lib == name) {
788 continue;
789 }
790 }
791 discover_recursive(&path, libs, files);
792 } else if let Some(name) = path.file_name().and_then(|n| n.to_str())
793 && name.ends_with(".sol")
794 {
795 files.push(path);
796 }
797 }
798}
799
800pub fn build_batch_standard_json_input(
807 source_files: &[PathBuf],
808 remappings: &[String],
809 config: &FoundryConfig,
810) -> Value {
811 build_batch_standard_json_input_with_cache(source_files, remappings, config, None)
812}
813
814pub fn build_batch_standard_json_input_with_cache(
824 source_files: &[PathBuf],
825 remappings: &[String],
826 config: &FoundryConfig,
827 content_cache: Option<&HashMap<String, (i32, String)>>,
828) -> Value {
829 let mut contract_outputs = vec!["abi", "devdoc", "userdoc", "evm.methodIdentifiers"];
830 if !config.via_ir {
831 contract_outputs.push("evm.gasEstimates");
832 }
833
834 let mut settings = json!({
835 "remappings": remappings,
836 "outputSelection": {
837 "*": {
838 "*": contract_outputs,
839 "": ["ast"]
840 }
841 }
842 });
843
844 if config.via_ir {
845 settings["viaIR"] = json!(true);
846 }
847 if let Some(ref evm_version) = config.evm_version {
848 settings["evmVersion"] = json!(evm_version);
849 }
850
851 let mut sources = serde_json::Map::new();
852 for file in source_files {
853 let rel_path = file
854 .strip_prefix(&config.root)
855 .map(|p| p.to_string_lossy().into_owned())
856 .unwrap_or_else(|_| file.to_string_lossy().into_owned());
857
858 let cached_content = content_cache.and_then(|cache| {
860 let uri = Url::from_file_path(file).ok()?;
861 cache.get(&uri.to_string()).map(|(_, c)| c.as_str())
862 });
863
864 if let Some(content) = cached_content {
865 sources.insert(rel_path, json!({ "content": content }));
866 } else {
867 sources.insert(rel_path.clone(), json!({ "urls": [rel_path] }));
868 }
869 }
870
871 json!({
872 "language": "Solidity",
873 "sources": sources,
874 "settings": settings
875 })
876}
877
878pub async fn solc_project_index(
888 config: &FoundryConfig,
889 client: Option<&tower_lsp::Client>,
890 text_cache: Option<&HashMap<String, (i32, String)>>,
891) -> Result<Value, RunnerError> {
892 let source_files = discover_source_files(config);
893 if source_files.is_empty() {
894 return Err(RunnerError::CommandError(std::io::Error::other(
895 "no source files found for project index",
896 )));
897 }
898
899 solc_project_index_from_files(config, client, text_cache, &source_files).await
900}
901
902pub async fn solc_project_index_scoped(
907 config: &FoundryConfig,
908 client: Option<&tower_lsp::Client>,
909 text_cache: Option<&HashMap<String, (i32, String)>>,
910 source_files: &[PathBuf],
911) -> Result<Value, RunnerError> {
912 if source_files.is_empty() {
913 return Err(RunnerError::CommandError(std::io::Error::other(
914 "no source files provided for scoped project index",
915 )));
916 }
917
918 solc_project_index_from_files(config, client, text_cache, source_files).await
919}
920
921async fn solc_project_index_from_files(
922 config: &FoundryConfig,
923 client: Option<&tower_lsp::Client>,
924 text_cache: Option<&HashMap<String, (i32, String)>>,
925 source_files: &[PathBuf],
926) -> Result<Value, RunnerError> {
927 if source_files.is_empty() {
928 return Err(RunnerError::CommandError(std::io::Error::other(
929 "no source files found for project index",
930 )));
931 }
932
933 if let Some(c) = client {
934 c.log_message(
935 tower_lsp::lsp_types::MessageType::INFO,
936 format!(
937 "project index: discovered {} source files in {}",
938 source_files.len(),
939 config.root.display()
940 ),
941 )
942 .await;
943 }
944
945 let first_source = text_cache
948 .and_then(|tc| {
949 let uri = Url::from_file_path(&source_files[0]).ok()?;
950 tc.get(&uri.to_string()).map(|(_, c)| c.clone())
951 })
952 .or_else(|| std::fs::read_to_string(&source_files[0]).ok());
953 let solc_binary = resolve_solc_binary(config, first_source.as_deref(), client).await;
954 let remappings = resolve_remappings(config).await;
955
956 let input =
957 build_batch_standard_json_input_with_cache(&source_files, &remappings, config, text_cache);
958 let raw_output = run_solc(&solc_binary, &input, &config.root).await?;
959 Ok(normalize_solc_output(raw_output, Some(&config.root)))
960}
961
962#[cfg(test)]
963mod tests {
964 use super::*;
965
966 #[test]
967 fn test_normalize_solc_sources() {
968 let solc_output = json!({
969 "sources": {
970 "src/Foo.sol": {
971 "id": 0,
972 "ast": {
973 "nodeType": "SourceUnit",
974 "absolutePath": "src/Foo.sol",
975 "id": 100
976 }
977 },
978 "src/Bar.sol": {
979 "id": 1,
980 "ast": {
981 "nodeType": "SourceUnit",
982 "absolutePath": "src/Bar.sol",
983 "id": 200
984 }
985 }
986 },
987 "contracts": {},
988 "errors": []
989 });
990
991 let normalized = normalize_solc_output(solc_output, None);
992
993 let sources = normalized.get("sources").unwrap().as_object().unwrap();
995 assert_eq!(sources.len(), 2);
996
997 let foo = sources.get("src/Foo.sol").unwrap();
998 assert_eq!(foo.get("id").unwrap(), 0);
999 assert_eq!(
1000 foo.get("ast")
1001 .unwrap()
1002 .get("nodeType")
1003 .unwrap()
1004 .as_str()
1005 .unwrap(),
1006 "SourceUnit"
1007 );
1008
1009 let id_to_path = normalized
1011 .get("source_id_to_path")
1012 .unwrap()
1013 .as_object()
1014 .unwrap();
1015 assert_eq!(id_to_path.len(), 2);
1016 }
1017
1018 #[test]
1019 fn test_normalize_solc_contracts() {
1020 let solc_output = json!({
1021 "sources": {},
1022 "contracts": {
1023 "src/Foo.sol": {
1024 "Foo": {
1025 "abi": [{"type": "function", "name": "bar"}],
1026 "evm": {
1027 "methodIdentifiers": {
1028 "bar(uint256)": "abcd1234"
1029 },
1030 "gasEstimates": {
1031 "external": {"bar(uint256)": "200"}
1032 }
1033 }
1034 }
1035 }
1036 },
1037 "errors": []
1038 });
1039
1040 let normalized = normalize_solc_output(solc_output, None);
1041
1042 let contracts = normalized.get("contracts").unwrap().as_object().unwrap();
1044 let foo_contracts = contracts.get("src/Foo.sol").unwrap().as_object().unwrap();
1045 let foo = foo_contracts.get("Foo").unwrap();
1046
1047 let method_ids = foo
1048 .get("evm")
1049 .unwrap()
1050 .get("methodIdentifiers")
1051 .unwrap()
1052 .as_object()
1053 .unwrap();
1054 assert_eq!(
1055 method_ids.get("bar(uint256)").unwrap().as_str().unwrap(),
1056 "abcd1234"
1057 );
1058 }
1059
1060 #[test]
1061 fn test_normalize_solc_errors_passthrough() {
1062 let solc_output = json!({
1063 "sources": {},
1064 "contracts": {},
1065 "errors": [{
1066 "sourceLocation": {"file": "src/Foo.sol", "start": 0, "end": 10},
1067 "type": "Warning",
1068 "component": "general",
1069 "severity": "warning",
1070 "errorCode": "2394",
1071 "message": "test warning",
1072 "formattedMessage": "Warning: test warning"
1073 }]
1074 });
1075
1076 let normalized = normalize_solc_output(solc_output, None);
1077
1078 let errors = normalized.get("errors").unwrap().as_array().unwrap();
1079 assert_eq!(errors.len(), 1);
1080 assert_eq!(
1081 errors[0].get("errorCode").unwrap().as_str().unwrap(),
1082 "2394"
1083 );
1084 }
1085
1086 #[test]
1087 fn test_normalize_empty_solc_output() {
1088 let solc_output = json!({
1089 "sources": {},
1090 "contracts": {}
1091 });
1092
1093 let normalized = normalize_solc_output(solc_output, None);
1094
1095 assert!(
1096 normalized
1097 .get("sources")
1098 .unwrap()
1099 .as_object()
1100 .unwrap()
1101 .is_empty()
1102 );
1103 assert!(
1104 normalized
1105 .get("contracts")
1106 .unwrap()
1107 .as_object()
1108 .unwrap()
1109 .is_empty()
1110 );
1111 assert_eq!(
1112 normalized.get("errors").unwrap().as_array().unwrap().len(),
1113 0
1114 );
1115 assert!(
1116 normalized
1117 .get("source_id_to_path")
1118 .unwrap()
1119 .as_object()
1120 .unwrap()
1121 .is_empty()
1122 );
1123 }
1124
1125 #[test]
1126 fn test_build_standard_json_input() {
1127 let config = FoundryConfig::default();
1128 let input = build_standard_json_input(
1129 "/path/to/Foo.sol",
1130 &[
1131 "ds-test/=lib/forge-std/lib/ds-test/src/".to_string(),
1132 "forge-std/=lib/forge-std/src/".to_string(),
1133 ],
1134 &config,
1135 );
1136
1137 let sources = input.get("sources").unwrap().as_object().unwrap();
1138 assert!(sources.contains_key("/path/to/Foo.sol"));
1139
1140 let settings = input.get("settings").unwrap();
1141 let remappings = settings.get("remappings").unwrap().as_array().unwrap();
1142 assert_eq!(remappings.len(), 2);
1143
1144 let output_sel = settings.get("outputSelection").unwrap();
1145 assert!(output_sel.get("*").is_some());
1146
1147 assert!(settings.get("optimizer").is_none());
1149 assert!(settings.get("viaIR").is_none());
1150 assert!(settings.get("evmVersion").is_none());
1151
1152 let outputs = settings["outputSelection"]["*"]["*"].as_array().unwrap();
1154 let output_names: Vec<&str> = outputs.iter().map(|v| v.as_str().unwrap()).collect();
1155 assert!(output_names.contains(&"evm.gasEstimates"));
1156 assert!(output_names.contains(&"abi"));
1157 assert!(output_names.contains(&"devdoc"));
1158 assert!(output_names.contains(&"userdoc"));
1159 assert!(output_names.contains(&"evm.methodIdentifiers"));
1160 }
1161
1162 #[test]
1163 fn test_build_standard_json_input_with_config() {
1164 let config = FoundryConfig {
1165 optimizer: true,
1166 optimizer_runs: 9999999,
1167 via_ir: true,
1168 evm_version: Some("osaka".to_string()),
1169 ..Default::default()
1170 };
1171 let input = build_standard_json_input("/path/to/Foo.sol", &[], &config);
1172
1173 let settings = input.get("settings").unwrap();
1174
1175 assert!(settings.get("optimizer").is_none());
1177
1178 assert!(settings.get("viaIR").unwrap().as_bool().unwrap());
1180
1181 let outputs = settings["outputSelection"]["*"]["*"].as_array().unwrap();
1183 let output_names: Vec<&str> = outputs.iter().map(|v| v.as_str().unwrap()).collect();
1184 assert!(!output_names.contains(&"evm.gasEstimates"));
1185
1186 assert_eq!(
1188 settings.get("evmVersion").unwrap().as_str().unwrap(),
1189 "osaka"
1190 );
1191 }
1192
1193 #[tokio::test]
1194 async fn test_resolve_solc_binary_default() {
1195 let config = FoundryConfig::default();
1196 let binary = resolve_solc_binary(&config, None, None).await;
1197 assert_eq!(binary, PathBuf::from("solc"));
1198 }
1199
1200 #[test]
1201 fn test_parse_pragma_exact() {
1202 let source = "// SPDX\npragma solidity 0.8.26;\n";
1203 assert_eq!(
1204 parse_pragma(source),
1205 Some(PragmaConstraint::Exact(SemVer {
1206 major: 0,
1207 minor: 8,
1208 patch: 26
1209 }))
1210 );
1211 }
1212
1213 #[test]
1214 fn test_parse_pragma_caret() {
1215 let source = "pragma solidity ^0.8.0;\n";
1216 assert_eq!(
1217 parse_pragma(source),
1218 Some(PragmaConstraint::Caret(SemVer {
1219 major: 0,
1220 minor: 8,
1221 patch: 0
1222 }))
1223 );
1224 }
1225
1226 #[test]
1227 fn test_parse_pragma_gte() {
1228 let source = "pragma solidity >=0.8.0;\n";
1229 assert_eq!(
1230 parse_pragma(source),
1231 Some(PragmaConstraint::Gte(SemVer {
1232 major: 0,
1233 minor: 8,
1234 patch: 0
1235 }))
1236 );
1237 }
1238
1239 #[test]
1240 fn test_parse_pragma_range() {
1241 let source = "pragma solidity >=0.6.2 <0.9.0;\n";
1242 assert_eq!(
1243 parse_pragma(source),
1244 Some(PragmaConstraint::Range(
1245 SemVer {
1246 major: 0,
1247 minor: 6,
1248 patch: 2
1249 },
1250 SemVer {
1251 major: 0,
1252 minor: 9,
1253 patch: 0
1254 },
1255 ))
1256 );
1257 }
1258
1259 #[test]
1260 fn test_parse_pragma_none() {
1261 let source = "contract Foo {}\n";
1262 assert_eq!(parse_pragma(source), None);
1263 }
1264
1265 #[test]
1266 fn test_version_satisfies_exact() {
1267 let v = SemVer {
1268 major: 0,
1269 minor: 8,
1270 patch: 26,
1271 };
1272 assert!(version_satisfies(&v, &PragmaConstraint::Exact(v.clone())));
1273 assert!(!version_satisfies(
1274 &SemVer {
1275 major: 0,
1276 minor: 8,
1277 patch: 25
1278 },
1279 &PragmaConstraint::Exact(v)
1280 ));
1281 }
1282
1283 #[test]
1284 fn test_version_satisfies_caret() {
1285 let constraint = PragmaConstraint::Caret(SemVer {
1286 major: 0,
1287 minor: 8,
1288 patch: 0,
1289 });
1290 assert!(version_satisfies(
1291 &SemVer {
1292 major: 0,
1293 minor: 8,
1294 patch: 0
1295 },
1296 &constraint
1297 ));
1298 assert!(version_satisfies(
1299 &SemVer {
1300 major: 0,
1301 minor: 8,
1302 patch: 26
1303 },
1304 &constraint
1305 ));
1306 assert!(!version_satisfies(
1308 &SemVer {
1309 major: 0,
1310 minor: 9,
1311 patch: 0
1312 },
1313 &constraint
1314 ));
1315 assert!(!version_satisfies(
1317 &SemVer {
1318 major: 0,
1319 minor: 7,
1320 patch: 0
1321 },
1322 &constraint
1323 ));
1324 }
1325
1326 #[test]
1327 fn test_version_satisfies_gte() {
1328 let constraint = PragmaConstraint::Gte(SemVer {
1329 major: 0,
1330 minor: 8,
1331 patch: 0,
1332 });
1333 assert!(version_satisfies(
1334 &SemVer {
1335 major: 0,
1336 minor: 8,
1337 patch: 0
1338 },
1339 &constraint
1340 ));
1341 assert!(version_satisfies(
1342 &SemVer {
1343 major: 0,
1344 minor: 9,
1345 patch: 0
1346 },
1347 &constraint
1348 ));
1349 assert!(!version_satisfies(
1350 &SemVer {
1351 major: 0,
1352 minor: 7,
1353 patch: 0
1354 },
1355 &constraint
1356 ));
1357 }
1358
1359 #[test]
1360 fn test_version_satisfies_range() {
1361 let constraint = PragmaConstraint::Range(
1362 SemVer {
1363 major: 0,
1364 minor: 6,
1365 patch: 2,
1366 },
1367 SemVer {
1368 major: 0,
1369 minor: 9,
1370 patch: 0,
1371 },
1372 );
1373 assert!(version_satisfies(
1374 &SemVer {
1375 major: 0,
1376 minor: 6,
1377 patch: 2
1378 },
1379 &constraint
1380 ));
1381 assert!(version_satisfies(
1382 &SemVer {
1383 major: 0,
1384 minor: 8,
1385 patch: 26
1386 },
1387 &constraint
1388 ));
1389 assert!(!version_satisfies(
1391 &SemVer {
1392 major: 0,
1393 minor: 9,
1394 patch: 0
1395 },
1396 &constraint
1397 ));
1398 assert!(!version_satisfies(
1399 &SemVer {
1400 major: 0,
1401 minor: 6,
1402 patch: 1
1403 },
1404 &constraint
1405 ));
1406 }
1407
1408 #[test]
1409 fn test_find_matching_version() {
1410 let installed = vec![
1411 SemVer {
1412 major: 0,
1413 minor: 8,
1414 patch: 0,
1415 },
1416 SemVer {
1417 major: 0,
1418 minor: 8,
1419 patch: 20,
1420 },
1421 SemVer {
1422 major: 0,
1423 minor: 8,
1424 patch: 26,
1425 },
1426 SemVer {
1427 major: 0,
1428 minor: 8,
1429 patch: 33,
1430 },
1431 ];
1432 let constraint = PragmaConstraint::Caret(SemVer {
1434 major: 0,
1435 minor: 8,
1436 patch: 20,
1437 });
1438 let matched = find_matching_version(&constraint, &installed);
1439 assert_eq!(
1440 matched,
1441 Some(SemVer {
1442 major: 0,
1443 minor: 8,
1444 patch: 33
1445 })
1446 );
1447
1448 let constraint = PragmaConstraint::Exact(SemVer {
1450 major: 0,
1451 minor: 8,
1452 patch: 20,
1453 });
1454 let matched = find_matching_version(&constraint, &installed);
1455 assert_eq!(
1456 matched,
1457 Some(SemVer {
1458 major: 0,
1459 minor: 8,
1460 patch: 20
1461 })
1462 );
1463
1464 let constraint = PragmaConstraint::Exact(SemVer {
1466 major: 0,
1467 minor: 8,
1468 patch: 15,
1469 });
1470 let matched = find_matching_version(&constraint, &installed);
1471 assert_eq!(matched, None);
1472 }
1473
1474 #[test]
1475 fn test_list_installed_versions() {
1476 let versions = list_installed_versions();
1478 for w in versions.windows(2) {
1480 assert!(w[0] <= w[1]);
1481 }
1482 }
1483}