1use crate::config::FoundryConfig;
8use crate::runner::RunnerError;
9use serde_json::{Map, Value, json};
10use std::path::{Path, PathBuf};
11use std::sync::{Mutex, OnceLock};
12use tokio::process::Command;
13
14static INSTALLED_VERSIONS: OnceLock<Mutex<Vec<SemVer>>> = OnceLock::new();
17
18fn get_installed_versions() -> Vec<SemVer> {
19 let mutex = INSTALLED_VERSIONS.get_or_init(|| Mutex::new(scan_installed_versions()));
20 mutex.lock().unwrap().clone()
21}
22
23fn invalidate_installed_versions() {
24 if let Some(mutex) = INSTALLED_VERSIONS.get() {
25 *mutex.lock().unwrap() = scan_installed_versions();
26 }
27}
28
29fn semver_to_local(v: &semver::Version) -> SemVer {
31 SemVer {
32 major: v.major as u32,
33 minor: v.minor as u32,
34 patch: v.patch as u32,
35 }
36}
37
38pub async fn resolve_solc_binary(
51 config: &FoundryConfig,
52 file_source: Option<&str>,
53 client: Option<&tower_lsp::Client>,
54) -> PathBuf {
55 if let Some(source) = file_source
57 && let Some(constraint) = parse_pragma(source)
58 {
59 if !matches!(constraint, PragmaConstraint::Exact(_))
65 && let Some(ref config_ver) = config.solc_version
66 && let Some(parsed) = SemVer::parse(config_ver)
67 && version_satisfies(&parsed, &constraint)
68 && let Some(path) = find_solc_binary(config_ver)
69 {
70 if let Some(c) = client {
71 c.log_message(
72 tower_lsp::lsp_types::MessageType::INFO,
73 format!(
74 "solc: foundry.toml {config_ver} satisfies pragma {constraint:?} → {}",
75 path.display()
76 ),
77 )
78 .await;
79 }
80 return path;
81 }
82
83 let installed = get_installed_versions();
84 if let Some(version) = find_matching_version(&constraint, &installed)
85 && let Some(path) = find_solc_binary(&version.to_string())
86 {
87 if let Some(c) = client {
88 c.log_message(
89 tower_lsp::lsp_types::MessageType::INFO,
90 format!(
91 "solc: pragma {constraint:?} → {version} → {}",
92 path.display()
93 ),
94 )
95 .await;
96 }
97 return path;
98 }
99
100 let install_version = version_to_install(&constraint);
102 if let Some(ref ver_str) = install_version {
103 if let Some(c) = client {
104 c.show_message(
105 tower_lsp::lsp_types::MessageType::INFO,
106 format!("Installing solc {ver_str}..."),
107 )
108 .await;
109 }
110
111 if svm_install(ver_str).await {
112 invalidate_installed_versions();
114
115 if let Some(c) = client {
116 c.show_message(
117 tower_lsp::lsp_types::MessageType::INFO,
118 format!("Installed solc {ver_str}"),
119 )
120 .await;
121 }
122 if let Some(path) = find_solc_binary(ver_str) {
123 return path;
124 }
125 } else if let Some(c) = client {
126 c.show_message(
127 tower_lsp::lsp_types::MessageType::WARNING,
128 format!(
129 "Failed to install solc {ver_str}. \
130 Install it manually: svm install {ver_str}"
131 ),
132 )
133 .await;
134 }
135 }
136 }
137
138 if let Some(ref version) = config.solc_version
140 && let Some(path) = find_solc_binary(version)
141 {
142 if let Some(c) = client {
143 c.log_message(
144 tower_lsp::lsp_types::MessageType::INFO,
145 format!(
146 "solc: no pragma, using foundry.toml version {version} → {}",
147 path.display()
148 ),
149 )
150 .await;
151 }
152 return path;
153 }
154
155 if let Some(c) = client {
157 c.log_message(
158 tower_lsp::lsp_types::MessageType::INFO,
159 "solc: no pragma match, falling back to system solc",
160 )
161 .await;
162 }
163 PathBuf::from("solc")
164}
165
166fn version_to_install(constraint: &PragmaConstraint) -> Option<String> {
173 match constraint {
174 PragmaConstraint::Exact(v) => Some(v.to_string()),
175 PragmaConstraint::Caret(v) => Some(v.to_string()),
176 PragmaConstraint::Gte(v) => Some(v.to_string()),
177 PragmaConstraint::Range(lower, _) => Some(lower.to_string()),
178 }
179}
180
181async fn svm_install(version: &str) -> bool {
185 let ver = match semver::Version::parse(version) {
186 Ok(v) => v,
187 Err(_) => return false,
188 };
189 svm::install(&ver).await.is_ok()
190}
191
192fn find_solc_binary(version: &str) -> Option<PathBuf> {
194 let path = svm::version_binary(version);
195 if path.is_file() {
196 return Some(path);
197 }
198 None
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
205pub struct SemVer {
206 pub major: u32,
207 pub minor: u32,
208 pub patch: u32,
209}
210
211impl SemVer {
212 fn parse(s: &str) -> Option<SemVer> {
213 let parts: Vec<&str> = s.split('.').collect();
214 if parts.len() != 3 {
215 return None;
216 }
217 Some(SemVer {
218 major: parts[0].parse().ok()?,
219 minor: parts[1].parse().ok()?,
220 patch: parts[2].parse().ok()?,
221 })
222 }
223}
224
225impl std::fmt::Display for SemVer {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
228 }
229}
230
231#[derive(Debug, Clone, PartialEq)]
233pub enum PragmaConstraint {
234 Exact(SemVer),
236 Caret(SemVer),
239 Gte(SemVer),
241 Range(SemVer, SemVer),
243}
244
245pub fn parse_pragma(source: &str) -> Option<PragmaConstraint> {
253 let pragma_line = source
255 .lines()
256 .take(20)
257 .find(|line| line.trim_start().starts_with("pragma solidity"))?;
258
259 let after_keyword = pragma_line
261 .trim_start()
262 .strip_prefix("pragma solidity")?
263 .trim();
264 let constraint_str = after_keyword
265 .strip_suffix(';')
266 .unwrap_or(after_keyword)
267 .trim();
268
269 if constraint_str.is_empty() {
270 return None;
271 }
272
273 if let Some(rest) = constraint_str.strip_prefix(">=") {
275 let rest = rest.trim();
276 if let Some(space_idx) = rest.find(|c: char| c.is_whitespace() || c == '<') {
277 let lower_str = rest[..space_idx].trim();
278 let upper_part = rest[space_idx..].trim();
279 if let Some(upper_str) = upper_part.strip_prefix('<') {
280 let upper_str = upper_str.trim();
281 if let (Some(lower), Some(upper)) =
282 (SemVer::parse(lower_str), SemVer::parse(upper_str))
283 {
284 return Some(PragmaConstraint::Range(lower, upper));
285 }
286 }
287 }
288 if let Some(ver) = SemVer::parse(rest) {
290 return Some(PragmaConstraint::Gte(ver));
291 }
292 }
293
294 if let Some(rest) = constraint_str.strip_prefix('^')
296 && let Some(ver) = SemVer::parse(rest.trim())
297 {
298 return Some(PragmaConstraint::Caret(ver));
299 }
300
301 if let Some(ver) = SemVer::parse(constraint_str) {
303 return Some(PragmaConstraint::Exact(ver));
304 }
305
306 None
307}
308
309pub fn list_installed_versions() -> Vec<SemVer> {
311 get_installed_versions()
312}
313
314fn scan_installed_versions() -> Vec<SemVer> {
318 svm::installed_versions()
319 .unwrap_or_default()
320 .iter()
321 .map(semver_to_local)
322 .collect()
323}
324
325pub fn find_matching_version(
330 constraint: &PragmaConstraint,
331 installed: &[SemVer],
332) -> Option<SemVer> {
333 let candidates: Vec<&SemVer> = installed
334 .iter()
335 .filter(|v| version_satisfies(v, constraint))
336 .collect();
337
338 candidates.last().cloned().cloned()
340}
341
342pub fn version_satisfies(version: &SemVer, constraint: &PragmaConstraint) -> bool {
344 match constraint {
345 PragmaConstraint::Exact(v) => version == v,
346 PragmaConstraint::Caret(v) => {
347 version.major == v.major && version >= v && version.minor < v.minor + 1
350 }
351 PragmaConstraint::Gte(v) => version >= v,
352 PragmaConstraint::Range(lower, upper) => version >= lower && version < upper,
353 }
354}
355
356pub async fn resolve_remappings(config: &FoundryConfig) -> Vec<String> {
360 let output = Command::new("forge")
363 .arg("remappings")
364 .current_dir(&config.root)
365 .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
366 .output()
367 .await;
368
369 if let Ok(output) = output
370 && output.status.success()
371 {
372 let stdout = String::from_utf8_lossy(&output.stdout);
373 let remappings: Vec<String> = stdout
374 .lines()
375 .filter(|l| !l.trim().is_empty())
376 .map(|l| l.to_string())
377 .collect();
378 if !remappings.is_empty() {
379 return remappings;
380 }
381 }
382
383 if !config.remappings.is_empty() {
385 return config.remappings.clone();
386 }
387
388 let remappings_txt = config.root.join("remappings.txt");
390 if let Ok(content) = std::fs::read_to_string(&remappings_txt) {
391 return content
392 .lines()
393 .filter(|l| !l.trim().is_empty())
394 .map(|l| l.to_string())
395 .collect();
396 }
397
398 Vec::new()
399}
400
401pub fn build_standard_json_input(
418 file_path: &str,
419 remappings: &[String],
420 config: &FoundryConfig,
421) -> Value {
422 let mut contract_outputs = vec!["abi", "devdoc", "userdoc", "evm.methodIdentifiers"];
425 if !config.via_ir {
426 contract_outputs.push("evm.gasEstimates");
427 }
428
429 let mut settings = json!({
430 "remappings": remappings,
431 "outputSelection": {
432 "*": {
433 "*": contract_outputs,
434 "": ["ast"]
435 }
436 }
437 });
438
439 if config.via_ir {
440 settings["viaIR"] = json!(true);
441 }
442
443 if let Some(ref evm_version) = config.evm_version {
445 settings["evmVersion"] = json!(evm_version);
446 }
447
448 json!({
449 "language": "Solidity",
450 "sources": {
451 file_path: {
452 "urls": [file_path]
453 }
454 },
455 "settings": settings
456 })
457}
458
459pub async fn run_solc(
461 solc_binary: &Path,
462 input: &Value,
463 project_root: &Path,
464) -> Result<Value, RunnerError> {
465 let input_str = serde_json::to_string(input)?;
466
467 let mut child = Command::new(solc_binary)
468 .arg("--standard-json")
469 .current_dir(project_root)
470 .stdin(std::process::Stdio::piped())
471 .stdout(std::process::Stdio::piped())
472 .stderr(std::process::Stdio::piped())
473 .spawn()?;
474
475 if let Some(mut stdin) = child.stdin.take() {
477 use tokio::io::AsyncWriteExt;
478 stdin
479 .write_all(input_str.as_bytes())
480 .await
481 .map_err(RunnerError::CommandError)?;
482 }
484
485 let output = child
486 .wait_with_output()
487 .await
488 .map_err(RunnerError::CommandError)?;
489
490 let stdout = String::from_utf8_lossy(&output.stdout);
492 if stdout.trim().is_empty() {
493 let stderr = String::from_utf8_lossy(&output.stderr);
494 return Err(RunnerError::CommandError(std::io::Error::other(format!(
495 "solc produced no output, stderr: {stderr}"
496 ))));
497 }
498
499 let parsed: Value = serde_json::from_str(&stdout)?;
500 Ok(parsed)
501}
502
503pub fn normalize_solc_output(mut solc_output: Value, project_root: Option<&Path>) -> Value {
521 let mut result = Map::new();
522
523 let errors = solc_output
525 .get_mut("errors")
526 .map(Value::take)
527 .unwrap_or_else(|| json!([]));
528 result.insert("errors".to_string(), errors);
529
530 let resolve = |p: &str| -> String {
533 if let Some(root) = project_root {
534 let path = Path::new(p);
535 if path.is_relative() {
536 return root.join(path).to_string_lossy().into_owned();
537 }
538 }
539 p.to_string()
540 };
541
542 let mut source_id_to_path = Map::new();
545 let mut resolved_sources = Map::new();
546
547 if let Some(sources) = solc_output
548 .get_mut("sources")
549 .and_then(|s| s.as_object_mut())
550 {
551 let keys: Vec<String> = sources.keys().cloned().collect();
553 for key in keys {
554 if let Some(mut source_data) = sources.remove(&key) {
555 let abs_key = resolve(&key);
556
557 if let Some(ast) = source_data.get_mut("ast")
559 && let Some(abs_path) = ast.get("absolutePath").and_then(|v| v.as_str())
560 {
561 let resolved = resolve(abs_path);
562 ast.as_object_mut()
563 .unwrap()
564 .insert("absolutePath".to_string(), json!(resolved));
565 }
566
567 if let Some(id) = source_data.get("id") {
568 source_id_to_path.insert(id.to_string(), json!(&abs_key));
569 }
570
571 resolved_sources.insert(abs_key, source_data);
572 }
573 }
574 }
575
576 result.insert("sources".to_string(), Value::Object(resolved_sources));
577
578 let mut resolved_contracts = Map::new();
580 if let Some(contracts) = solc_output
581 .get_mut("contracts")
582 .and_then(|c| c.as_object_mut())
583 {
584 let keys: Vec<String> = contracts.keys().cloned().collect();
585 for key in keys {
586 if let Some(contract_data) = contracts.remove(&key) {
587 resolved_contracts.insert(resolve(&key), contract_data);
588 }
589 }
590 }
591 result.insert("contracts".to_string(), Value::Object(resolved_contracts));
592
593 result.insert(
595 "source_id_to_path".to_string(),
596 Value::Object(source_id_to_path),
597 );
598
599 Value::Object(result)
600}
601
602pub fn normalize_forge_output(mut forge_output: Value) -> Value {
614 let mut result = Map::new();
615
616 let errors = forge_output
618 .get_mut("errors")
619 .map(Value::take)
620 .unwrap_or_else(|| json!([]));
621 result.insert("errors".to_string(), errors);
622
623 let mut normalized_sources = Map::new();
625 if let Some(sources) = forge_output
626 .get_mut("sources")
627 .and_then(|s| s.as_object_mut())
628 {
629 for (path, entries) in sources.iter_mut() {
630 if let Some(arr) = entries.as_array_mut()
631 && let Some(first) = arr.first_mut()
632 && let Some(sf) = first.get_mut("source_file")
633 {
634 normalized_sources.insert(path.clone(), sf.take());
635 }
636 }
637 }
638 result.insert("sources".to_string(), Value::Object(normalized_sources));
639
640 let mut normalized_contracts = Map::new();
642 if let Some(contracts) = forge_output
643 .get_mut("contracts")
644 .and_then(|c| c.as_object_mut())
645 {
646 for (path, names) in contracts.iter_mut() {
647 let mut path_contracts = Map::new();
648 if let Some(names_obj) = names.as_object_mut() {
649 for (name, entries) in names_obj.iter_mut() {
650 if let Some(arr) = entries.as_array_mut()
651 && let Some(first) = arr.first_mut()
652 && let Some(contract) = first.get_mut("contract")
653 {
654 path_contracts.insert(name.clone(), contract.take());
655 }
656 }
657 }
658 normalized_contracts.insert(path.clone(), Value::Object(path_contracts));
659 }
660 }
661 result.insert("contracts".to_string(), Value::Object(normalized_contracts));
662
663 let source_id_to_path = forge_output
665 .get_mut("build_infos")
666 .and_then(|bi| bi.as_array_mut())
667 .and_then(|arr| arr.first_mut())
668 .and_then(|info| info.get_mut("source_id_to_path"))
669 .map(Value::take)
670 .unwrap_or_else(|| json!({}));
671 result.insert("source_id_to_path".to_string(), source_id_to_path);
672
673 Value::Object(result)
674}
675
676pub async fn solc_ast(
681 file_path: &str,
682 config: &FoundryConfig,
683 client: Option<&tower_lsp::Client>,
684) -> Result<Value, RunnerError> {
685 let file_source = std::fs::read_to_string(file_path).ok();
687 let solc_binary = resolve_solc_binary(config, file_source.as_deref(), client).await;
688 let remappings = resolve_remappings(config).await;
689
690 let rel_path = Path::new(file_path)
695 .strip_prefix(&config.root)
696 .map(|p| p.to_string_lossy().into_owned())
697 .unwrap_or_else(|_| file_path.to_string());
698
699 let input = build_standard_json_input(&rel_path, &remappings, config);
700 let raw_output = run_solc(&solc_binary, &input, &config.root).await?;
701
702 Ok(normalize_solc_output(raw_output, Some(&config.root)))
703}
704
705pub async fn solc_build(
707 file_path: &str,
708 config: &FoundryConfig,
709 client: Option<&tower_lsp::Client>,
710) -> Result<Value, RunnerError> {
711 solc_ast(file_path, config, client).await
712}
713
714const SKIP_DIRS: &[&str] = &[
718 "test",
719 "tests",
720 "lib",
721 "node_modules",
722 "out",
723 "artifacts",
724 "cache",
725 "script",
726 "scripts",
727];
728
729pub fn discover_source_files(config: &FoundryConfig) -> Vec<PathBuf> {
736 let sources_path = config.root.join(&config.sources_dir);
737 if !sources_path.is_dir() {
738 return Vec::new();
739 }
740 let mut files = Vec::new();
741 discover_recursive(&sources_path, &mut files);
742 files.sort();
743 files
744}
745
746fn discover_recursive(dir: &Path, files: &mut Vec<PathBuf>) {
747 let entries = match std::fs::read_dir(dir) {
748 Ok(e) => e,
749 Err(_) => return,
750 };
751 for entry in entries.flatten() {
752 let path = entry.path();
753 if path.is_dir() {
754 if let Some(name) = path.file_name().and_then(|n| n.to_str())
755 && SKIP_DIRS.contains(&name)
756 {
757 continue;
758 }
759 discover_recursive(&path, files);
760 } else if let Some(name) = path.file_name().and_then(|n| n.to_str())
761 && name.ends_with(".sol")
762 && !name.ends_with(".t.sol")
763 && !name.ends_with(".s.sol")
764 {
765 files.push(path);
766 }
767 }
768}
769
770pub fn build_batch_standard_json_input(
777 source_files: &[PathBuf],
778 remappings: &[String],
779 config: &FoundryConfig,
780) -> Value {
781 let mut contract_outputs = vec!["abi", "devdoc", "userdoc", "evm.methodIdentifiers"];
782 if !config.via_ir {
783 contract_outputs.push("evm.gasEstimates");
784 }
785
786 let mut settings = json!({
787 "remappings": remappings,
788 "outputSelection": {
789 "*": {
790 "*": contract_outputs,
791 "": ["ast"]
792 }
793 }
794 });
795
796 if config.via_ir {
797 settings["viaIR"] = json!(true);
798 }
799 if let Some(ref evm_version) = config.evm_version {
800 settings["evmVersion"] = json!(evm_version);
801 }
802
803 let mut sources = serde_json::Map::new();
804 for file in source_files {
805 let rel_path = file
806 .strip_prefix(&config.root)
807 .map(|p| p.to_string_lossy().into_owned())
808 .unwrap_or_else(|_| file.to_string_lossy().into_owned());
809 sources.insert(rel_path.clone(), json!({ "urls": [rel_path] }));
810 }
811
812 json!({
813 "language": "Solidity",
814 "sources": sources,
815 "settings": settings
816 })
817}
818
819pub async fn solc_project_index(
824 config: &FoundryConfig,
825 client: Option<&tower_lsp::Client>,
826) -> Result<Value, RunnerError> {
827 let source_files = discover_source_files(config);
828 if source_files.is_empty() {
829 return Err(RunnerError::CommandError(std::io::Error::other(
830 "no source files found for project index",
831 )));
832 }
833
834 if let Some(c) = client {
835 c.log_message(
836 tower_lsp::lsp_types::MessageType::INFO,
837 format!(
838 "project index: discovered {} source files in {}/",
839 source_files.len(),
840 config.sources_dir
841 ),
842 )
843 .await;
844 }
845
846 let first_source = std::fs::read_to_string(&source_files[0]).ok();
848 let solc_binary = resolve_solc_binary(config, first_source.as_deref(), client).await;
849 let remappings = resolve_remappings(config).await;
850
851 let input = build_batch_standard_json_input(&source_files, &remappings, config);
852 let raw_output = run_solc(&solc_binary, &input, &config.root).await?;
853 Ok(normalize_solc_output(raw_output, Some(&config.root)))
854}
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859
860 #[test]
861 fn test_normalize_solc_sources() {
862 let solc_output = json!({
863 "sources": {
864 "src/Foo.sol": {
865 "id": 0,
866 "ast": {
867 "nodeType": "SourceUnit",
868 "absolutePath": "src/Foo.sol",
869 "id": 100
870 }
871 },
872 "src/Bar.sol": {
873 "id": 1,
874 "ast": {
875 "nodeType": "SourceUnit",
876 "absolutePath": "src/Bar.sol",
877 "id": 200
878 }
879 }
880 },
881 "contracts": {},
882 "errors": []
883 });
884
885 let normalized = normalize_solc_output(solc_output, None);
886
887 let sources = normalized.get("sources").unwrap().as_object().unwrap();
889 assert_eq!(sources.len(), 2);
890
891 let foo = sources.get("src/Foo.sol").unwrap();
892 assert_eq!(foo.get("id").unwrap(), 0);
893 assert_eq!(
894 foo.get("ast")
895 .unwrap()
896 .get("nodeType")
897 .unwrap()
898 .as_str()
899 .unwrap(),
900 "SourceUnit"
901 );
902
903 let id_to_path = normalized
905 .get("source_id_to_path")
906 .unwrap()
907 .as_object()
908 .unwrap();
909 assert_eq!(id_to_path.len(), 2);
910 }
911
912 #[test]
913 fn test_normalize_solc_contracts() {
914 let solc_output = json!({
915 "sources": {},
916 "contracts": {
917 "src/Foo.sol": {
918 "Foo": {
919 "abi": [{"type": "function", "name": "bar"}],
920 "evm": {
921 "methodIdentifiers": {
922 "bar(uint256)": "abcd1234"
923 },
924 "gasEstimates": {
925 "external": {"bar(uint256)": "200"}
926 }
927 }
928 }
929 }
930 },
931 "errors": []
932 });
933
934 let normalized = normalize_solc_output(solc_output, None);
935
936 let contracts = normalized.get("contracts").unwrap().as_object().unwrap();
938 let foo_contracts = contracts.get("src/Foo.sol").unwrap().as_object().unwrap();
939 let foo = foo_contracts.get("Foo").unwrap();
940
941 let method_ids = foo
942 .get("evm")
943 .unwrap()
944 .get("methodIdentifiers")
945 .unwrap()
946 .as_object()
947 .unwrap();
948 assert_eq!(
949 method_ids.get("bar(uint256)").unwrap().as_str().unwrap(),
950 "abcd1234"
951 );
952 }
953
954 #[test]
955 fn test_normalize_solc_errors_passthrough() {
956 let solc_output = json!({
957 "sources": {},
958 "contracts": {},
959 "errors": [{
960 "sourceLocation": {"file": "src/Foo.sol", "start": 0, "end": 10},
961 "type": "Warning",
962 "component": "general",
963 "severity": "warning",
964 "errorCode": "2394",
965 "message": "test warning",
966 "formattedMessage": "Warning: test warning"
967 }]
968 });
969
970 let normalized = normalize_solc_output(solc_output, None);
971
972 let errors = normalized.get("errors").unwrap().as_array().unwrap();
973 assert_eq!(errors.len(), 1);
974 assert_eq!(
975 errors[0].get("errorCode").unwrap().as_str().unwrap(),
976 "2394"
977 );
978 }
979
980 #[test]
981 fn test_normalize_empty_solc_output() {
982 let solc_output = json!({
983 "sources": {},
984 "contracts": {}
985 });
986
987 let normalized = normalize_solc_output(solc_output, None);
988
989 assert!(
990 normalized
991 .get("sources")
992 .unwrap()
993 .as_object()
994 .unwrap()
995 .is_empty()
996 );
997 assert!(
998 normalized
999 .get("contracts")
1000 .unwrap()
1001 .as_object()
1002 .unwrap()
1003 .is_empty()
1004 );
1005 assert_eq!(
1006 normalized.get("errors").unwrap().as_array().unwrap().len(),
1007 0
1008 );
1009 assert!(
1010 normalized
1011 .get("source_id_to_path")
1012 .unwrap()
1013 .as_object()
1014 .unwrap()
1015 .is_empty()
1016 );
1017 }
1018
1019 #[test]
1020 fn test_build_standard_json_input() {
1021 let config = FoundryConfig::default();
1022 let input = build_standard_json_input(
1023 "/path/to/Foo.sol",
1024 &[
1025 "ds-test/=lib/forge-std/lib/ds-test/src/".to_string(),
1026 "forge-std/=lib/forge-std/src/".to_string(),
1027 ],
1028 &config,
1029 );
1030
1031 let sources = input.get("sources").unwrap().as_object().unwrap();
1032 assert!(sources.contains_key("/path/to/Foo.sol"));
1033
1034 let settings = input.get("settings").unwrap();
1035 let remappings = settings.get("remappings").unwrap().as_array().unwrap();
1036 assert_eq!(remappings.len(), 2);
1037
1038 let output_sel = settings.get("outputSelection").unwrap();
1039 assert!(output_sel.get("*").is_some());
1040
1041 assert!(settings.get("optimizer").is_none());
1043 assert!(settings.get("viaIR").is_none());
1044 assert!(settings.get("evmVersion").is_none());
1045
1046 let outputs = settings["outputSelection"]["*"]["*"].as_array().unwrap();
1048 let output_names: Vec<&str> = outputs.iter().map(|v| v.as_str().unwrap()).collect();
1049 assert!(output_names.contains(&"evm.gasEstimates"));
1050 assert!(output_names.contains(&"abi"));
1051 assert!(output_names.contains(&"devdoc"));
1052 assert!(output_names.contains(&"userdoc"));
1053 assert!(output_names.contains(&"evm.methodIdentifiers"));
1054 }
1055
1056 #[test]
1057 fn test_build_standard_json_input_with_config() {
1058 let config = FoundryConfig {
1059 optimizer: true,
1060 optimizer_runs: 9999999,
1061 via_ir: true,
1062 evm_version: Some("osaka".to_string()),
1063 ..Default::default()
1064 };
1065 let input = build_standard_json_input("/path/to/Foo.sol", &[], &config);
1066
1067 let settings = input.get("settings").unwrap();
1068
1069 assert!(settings.get("optimizer").is_none());
1071
1072 assert!(settings.get("viaIR").unwrap().as_bool().unwrap());
1074
1075 let outputs = settings["outputSelection"]["*"]["*"].as_array().unwrap();
1077 let output_names: Vec<&str> = outputs.iter().map(|v| v.as_str().unwrap()).collect();
1078 assert!(!output_names.contains(&"evm.gasEstimates"));
1079
1080 assert_eq!(
1082 settings.get("evmVersion").unwrap().as_str().unwrap(),
1083 "osaka"
1084 );
1085 }
1086
1087 #[tokio::test]
1088 async fn test_resolve_solc_binary_default() {
1089 let config = FoundryConfig::default();
1090 let binary = resolve_solc_binary(&config, None, None).await;
1091 assert_eq!(binary, PathBuf::from("solc"));
1092 }
1093
1094 #[test]
1095 fn test_parse_pragma_exact() {
1096 let source = "// SPDX\npragma solidity 0.8.26;\n";
1097 assert_eq!(
1098 parse_pragma(source),
1099 Some(PragmaConstraint::Exact(SemVer {
1100 major: 0,
1101 minor: 8,
1102 patch: 26
1103 }))
1104 );
1105 }
1106
1107 #[test]
1108 fn test_parse_pragma_caret() {
1109 let source = "pragma solidity ^0.8.0;\n";
1110 assert_eq!(
1111 parse_pragma(source),
1112 Some(PragmaConstraint::Caret(SemVer {
1113 major: 0,
1114 minor: 8,
1115 patch: 0
1116 }))
1117 );
1118 }
1119
1120 #[test]
1121 fn test_parse_pragma_gte() {
1122 let source = "pragma solidity >=0.8.0;\n";
1123 assert_eq!(
1124 parse_pragma(source),
1125 Some(PragmaConstraint::Gte(SemVer {
1126 major: 0,
1127 minor: 8,
1128 patch: 0
1129 }))
1130 );
1131 }
1132
1133 #[test]
1134 fn test_parse_pragma_range() {
1135 let source = "pragma solidity >=0.6.2 <0.9.0;\n";
1136 assert_eq!(
1137 parse_pragma(source),
1138 Some(PragmaConstraint::Range(
1139 SemVer {
1140 major: 0,
1141 minor: 6,
1142 patch: 2
1143 },
1144 SemVer {
1145 major: 0,
1146 minor: 9,
1147 patch: 0
1148 },
1149 ))
1150 );
1151 }
1152
1153 #[test]
1154 fn test_parse_pragma_none() {
1155 let source = "contract Foo {}\n";
1156 assert_eq!(parse_pragma(source), None);
1157 }
1158
1159 #[test]
1160 fn test_version_satisfies_exact() {
1161 let v = SemVer {
1162 major: 0,
1163 minor: 8,
1164 patch: 26,
1165 };
1166 assert!(version_satisfies(&v, &PragmaConstraint::Exact(v.clone())));
1167 assert!(!version_satisfies(
1168 &SemVer {
1169 major: 0,
1170 minor: 8,
1171 patch: 25
1172 },
1173 &PragmaConstraint::Exact(v)
1174 ));
1175 }
1176
1177 #[test]
1178 fn test_version_satisfies_caret() {
1179 let constraint = PragmaConstraint::Caret(SemVer {
1180 major: 0,
1181 minor: 8,
1182 patch: 0,
1183 });
1184 assert!(version_satisfies(
1185 &SemVer {
1186 major: 0,
1187 minor: 8,
1188 patch: 0
1189 },
1190 &constraint
1191 ));
1192 assert!(version_satisfies(
1193 &SemVer {
1194 major: 0,
1195 minor: 8,
1196 patch: 26
1197 },
1198 &constraint
1199 ));
1200 assert!(!version_satisfies(
1202 &SemVer {
1203 major: 0,
1204 minor: 9,
1205 patch: 0
1206 },
1207 &constraint
1208 ));
1209 assert!(!version_satisfies(
1211 &SemVer {
1212 major: 0,
1213 minor: 7,
1214 patch: 0
1215 },
1216 &constraint
1217 ));
1218 }
1219
1220 #[test]
1221 fn test_version_satisfies_gte() {
1222 let constraint = PragmaConstraint::Gte(SemVer {
1223 major: 0,
1224 minor: 8,
1225 patch: 0,
1226 });
1227 assert!(version_satisfies(
1228 &SemVer {
1229 major: 0,
1230 minor: 8,
1231 patch: 0
1232 },
1233 &constraint
1234 ));
1235 assert!(version_satisfies(
1236 &SemVer {
1237 major: 0,
1238 minor: 9,
1239 patch: 0
1240 },
1241 &constraint
1242 ));
1243 assert!(!version_satisfies(
1244 &SemVer {
1245 major: 0,
1246 minor: 7,
1247 patch: 0
1248 },
1249 &constraint
1250 ));
1251 }
1252
1253 #[test]
1254 fn test_version_satisfies_range() {
1255 let constraint = PragmaConstraint::Range(
1256 SemVer {
1257 major: 0,
1258 minor: 6,
1259 patch: 2,
1260 },
1261 SemVer {
1262 major: 0,
1263 minor: 9,
1264 patch: 0,
1265 },
1266 );
1267 assert!(version_satisfies(
1268 &SemVer {
1269 major: 0,
1270 minor: 6,
1271 patch: 2
1272 },
1273 &constraint
1274 ));
1275 assert!(version_satisfies(
1276 &SemVer {
1277 major: 0,
1278 minor: 8,
1279 patch: 26
1280 },
1281 &constraint
1282 ));
1283 assert!(!version_satisfies(
1285 &SemVer {
1286 major: 0,
1287 minor: 9,
1288 patch: 0
1289 },
1290 &constraint
1291 ));
1292 assert!(!version_satisfies(
1293 &SemVer {
1294 major: 0,
1295 minor: 6,
1296 patch: 1
1297 },
1298 &constraint
1299 ));
1300 }
1301
1302 #[test]
1303 fn test_find_matching_version() {
1304 let installed = vec![
1305 SemVer {
1306 major: 0,
1307 minor: 8,
1308 patch: 0,
1309 },
1310 SemVer {
1311 major: 0,
1312 minor: 8,
1313 patch: 20,
1314 },
1315 SemVer {
1316 major: 0,
1317 minor: 8,
1318 patch: 26,
1319 },
1320 SemVer {
1321 major: 0,
1322 minor: 8,
1323 patch: 33,
1324 },
1325 ];
1326 let constraint = PragmaConstraint::Caret(SemVer {
1328 major: 0,
1329 minor: 8,
1330 patch: 20,
1331 });
1332 let matched = find_matching_version(&constraint, &installed);
1333 assert_eq!(
1334 matched,
1335 Some(SemVer {
1336 major: 0,
1337 minor: 8,
1338 patch: 33
1339 })
1340 );
1341
1342 let constraint = PragmaConstraint::Exact(SemVer {
1344 major: 0,
1345 minor: 8,
1346 patch: 20,
1347 });
1348 let matched = find_matching_version(&constraint, &installed);
1349 assert_eq!(
1350 matched,
1351 Some(SemVer {
1352 major: 0,
1353 minor: 8,
1354 patch: 20
1355 })
1356 );
1357
1358 let constraint = PragmaConstraint::Exact(SemVer {
1360 major: 0,
1361 minor: 8,
1362 patch: 15,
1363 });
1364 let matched = find_matching_version(&constraint, &installed);
1365 assert_eq!(matched, None);
1366 }
1367
1368 #[test]
1369 fn test_list_installed_versions() {
1370 let versions = list_installed_versions();
1372 for w in versions.windows(2) {
1374 assert!(w[0] <= w[1]);
1375 }
1376 }
1377}