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 if SKIP_DIRS.contains(&name) {
756 continue;
757 }
758 }
759 discover_recursive(&path, files);
760 } else if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
761 if name.ends_with(".sol") && !name.ends_with(".t.sol") && !name.ends_with(".s.sol") {
762 files.push(path);
763 }
764 }
765 }
766}
767
768pub fn build_batch_standard_json_input(
775 source_files: &[PathBuf],
776 remappings: &[String],
777 config: &FoundryConfig,
778) -> Value {
779 let mut contract_outputs = vec!["abi", "devdoc", "userdoc", "evm.methodIdentifiers"];
780 if !config.via_ir {
781 contract_outputs.push("evm.gasEstimates");
782 }
783
784 let mut settings = json!({
785 "remappings": remappings,
786 "outputSelection": {
787 "*": {
788 "*": contract_outputs,
789 "": ["ast"]
790 }
791 }
792 });
793
794 if config.via_ir {
795 settings["viaIR"] = json!(true);
796 }
797 if let Some(ref evm_version) = config.evm_version {
798 settings["evmVersion"] = json!(evm_version);
799 }
800
801 let mut sources = serde_json::Map::new();
802 for file in source_files {
803 let rel_path = file
804 .strip_prefix(&config.root)
805 .map(|p| p.to_string_lossy().into_owned())
806 .unwrap_or_else(|_| file.to_string_lossy().into_owned());
807 sources.insert(rel_path.clone(), json!({ "urls": [rel_path] }));
808 }
809
810 json!({
811 "language": "Solidity",
812 "sources": sources,
813 "settings": settings
814 })
815}
816
817pub async fn solc_project_index(
822 config: &FoundryConfig,
823 client: Option<&tower_lsp::Client>,
824) -> Result<Value, RunnerError> {
825 let source_files = discover_source_files(config);
826 if source_files.is_empty() {
827 return Err(RunnerError::CommandError(std::io::Error::other(
828 "no source files found for project index",
829 )));
830 }
831
832 if let Some(c) = client {
833 c.log_message(
834 tower_lsp::lsp_types::MessageType::INFO,
835 format!(
836 "project index: discovered {} source files in {}/",
837 source_files.len(),
838 config.sources_dir
839 ),
840 )
841 .await;
842 }
843
844 let first_source = std::fs::read_to_string(&source_files[0]).ok();
846 let solc_binary = resolve_solc_binary(config, first_source.as_deref(), client).await;
847 let remappings = resolve_remappings(config).await;
848
849 let input = build_batch_standard_json_input(&source_files, &remappings, config);
850 let raw_output = run_solc(&solc_binary, &input, &config.root).await?;
851 Ok(normalize_solc_output(raw_output, Some(&config.root)))
852}
853
854#[cfg(test)]
855mod tests {
856 use super::*;
857
858 #[test]
859 fn test_normalize_solc_sources() {
860 let solc_output = json!({
861 "sources": {
862 "src/Foo.sol": {
863 "id": 0,
864 "ast": {
865 "nodeType": "SourceUnit",
866 "absolutePath": "src/Foo.sol",
867 "id": 100
868 }
869 },
870 "src/Bar.sol": {
871 "id": 1,
872 "ast": {
873 "nodeType": "SourceUnit",
874 "absolutePath": "src/Bar.sol",
875 "id": 200
876 }
877 }
878 },
879 "contracts": {},
880 "errors": []
881 });
882
883 let normalized = normalize_solc_output(solc_output, None);
884
885 let sources = normalized.get("sources").unwrap().as_object().unwrap();
887 assert_eq!(sources.len(), 2);
888
889 let foo = sources.get("src/Foo.sol").unwrap();
890 assert_eq!(foo.get("id").unwrap(), 0);
891 assert_eq!(
892 foo.get("ast")
893 .unwrap()
894 .get("nodeType")
895 .unwrap()
896 .as_str()
897 .unwrap(),
898 "SourceUnit"
899 );
900
901 let id_to_path = normalized
903 .get("source_id_to_path")
904 .unwrap()
905 .as_object()
906 .unwrap();
907 assert_eq!(id_to_path.len(), 2);
908 }
909
910 #[test]
911 fn test_normalize_solc_contracts() {
912 let solc_output = json!({
913 "sources": {},
914 "contracts": {
915 "src/Foo.sol": {
916 "Foo": {
917 "abi": [{"type": "function", "name": "bar"}],
918 "evm": {
919 "methodIdentifiers": {
920 "bar(uint256)": "abcd1234"
921 },
922 "gasEstimates": {
923 "external": {"bar(uint256)": "200"}
924 }
925 }
926 }
927 }
928 },
929 "errors": []
930 });
931
932 let normalized = normalize_solc_output(solc_output, None);
933
934 let contracts = normalized.get("contracts").unwrap().as_object().unwrap();
936 let foo_contracts = contracts.get("src/Foo.sol").unwrap().as_object().unwrap();
937 let foo = foo_contracts.get("Foo").unwrap();
938
939 let method_ids = foo
940 .get("evm")
941 .unwrap()
942 .get("methodIdentifiers")
943 .unwrap()
944 .as_object()
945 .unwrap();
946 assert_eq!(
947 method_ids.get("bar(uint256)").unwrap().as_str().unwrap(),
948 "abcd1234"
949 );
950 }
951
952 #[test]
953 fn test_normalize_solc_errors_passthrough() {
954 let solc_output = json!({
955 "sources": {},
956 "contracts": {},
957 "errors": [{
958 "sourceLocation": {"file": "src/Foo.sol", "start": 0, "end": 10},
959 "type": "Warning",
960 "component": "general",
961 "severity": "warning",
962 "errorCode": "2394",
963 "message": "test warning",
964 "formattedMessage": "Warning: test warning"
965 }]
966 });
967
968 let normalized = normalize_solc_output(solc_output, None);
969
970 let errors = normalized.get("errors").unwrap().as_array().unwrap();
971 assert_eq!(errors.len(), 1);
972 assert_eq!(
973 errors[0].get("errorCode").unwrap().as_str().unwrap(),
974 "2394"
975 );
976 }
977
978 #[test]
979 fn test_normalize_empty_solc_output() {
980 let solc_output = json!({
981 "sources": {},
982 "contracts": {}
983 });
984
985 let normalized = normalize_solc_output(solc_output, None);
986
987 assert!(
988 normalized
989 .get("sources")
990 .unwrap()
991 .as_object()
992 .unwrap()
993 .is_empty()
994 );
995 assert!(
996 normalized
997 .get("contracts")
998 .unwrap()
999 .as_object()
1000 .unwrap()
1001 .is_empty()
1002 );
1003 assert_eq!(
1004 normalized.get("errors").unwrap().as_array().unwrap().len(),
1005 0
1006 );
1007 assert!(
1008 normalized
1009 .get("source_id_to_path")
1010 .unwrap()
1011 .as_object()
1012 .unwrap()
1013 .is_empty()
1014 );
1015 }
1016
1017 #[test]
1018 fn test_build_standard_json_input() {
1019 let config = FoundryConfig::default();
1020 let input = build_standard_json_input(
1021 "/path/to/Foo.sol",
1022 &[
1023 "ds-test/=lib/forge-std/lib/ds-test/src/".to_string(),
1024 "forge-std/=lib/forge-std/src/".to_string(),
1025 ],
1026 &config,
1027 );
1028
1029 let sources = input.get("sources").unwrap().as_object().unwrap();
1030 assert!(sources.contains_key("/path/to/Foo.sol"));
1031
1032 let settings = input.get("settings").unwrap();
1033 let remappings = settings.get("remappings").unwrap().as_array().unwrap();
1034 assert_eq!(remappings.len(), 2);
1035
1036 let output_sel = settings.get("outputSelection").unwrap();
1037 assert!(output_sel.get("*").is_some());
1038
1039 assert!(settings.get("optimizer").is_none());
1041 assert!(settings.get("viaIR").is_none());
1042 assert!(settings.get("evmVersion").is_none());
1043
1044 let outputs = settings["outputSelection"]["*"]["*"].as_array().unwrap();
1046 let output_names: Vec<&str> = outputs.iter().map(|v| v.as_str().unwrap()).collect();
1047 assert!(output_names.contains(&"evm.gasEstimates"));
1048 assert!(output_names.contains(&"abi"));
1049 assert!(output_names.contains(&"devdoc"));
1050 assert!(output_names.contains(&"userdoc"));
1051 assert!(output_names.contains(&"evm.methodIdentifiers"));
1052 }
1053
1054 #[test]
1055 fn test_build_standard_json_input_with_config() {
1056 let config = FoundryConfig {
1057 optimizer: true,
1058 optimizer_runs: 9999999,
1059 via_ir: true,
1060 evm_version: Some("osaka".to_string()),
1061 ..Default::default()
1062 };
1063 let input = build_standard_json_input("/path/to/Foo.sol", &[], &config);
1064
1065 let settings = input.get("settings").unwrap();
1066
1067 assert!(settings.get("optimizer").is_none());
1069
1070 assert!(settings.get("viaIR").unwrap().as_bool().unwrap());
1072
1073 let outputs = settings["outputSelection"]["*"]["*"].as_array().unwrap();
1075 let output_names: Vec<&str> = outputs.iter().map(|v| v.as_str().unwrap()).collect();
1076 assert!(!output_names.contains(&"evm.gasEstimates"));
1077
1078 assert_eq!(
1080 settings.get("evmVersion").unwrap().as_str().unwrap(),
1081 "osaka"
1082 );
1083 }
1084
1085 #[tokio::test]
1086 async fn test_resolve_solc_binary_default() {
1087 let config = FoundryConfig::default();
1088 let binary = resolve_solc_binary(&config, None, None).await;
1089 assert_eq!(binary, PathBuf::from("solc"));
1090 }
1091
1092 #[test]
1093 fn test_parse_pragma_exact() {
1094 let source = "// SPDX\npragma solidity 0.8.26;\n";
1095 assert_eq!(
1096 parse_pragma(source),
1097 Some(PragmaConstraint::Exact(SemVer {
1098 major: 0,
1099 minor: 8,
1100 patch: 26
1101 }))
1102 );
1103 }
1104
1105 #[test]
1106 fn test_parse_pragma_caret() {
1107 let source = "pragma solidity ^0.8.0;\n";
1108 assert_eq!(
1109 parse_pragma(source),
1110 Some(PragmaConstraint::Caret(SemVer {
1111 major: 0,
1112 minor: 8,
1113 patch: 0
1114 }))
1115 );
1116 }
1117
1118 #[test]
1119 fn test_parse_pragma_gte() {
1120 let source = "pragma solidity >=0.8.0;\n";
1121 assert_eq!(
1122 parse_pragma(source),
1123 Some(PragmaConstraint::Gte(SemVer {
1124 major: 0,
1125 minor: 8,
1126 patch: 0
1127 }))
1128 );
1129 }
1130
1131 #[test]
1132 fn test_parse_pragma_range() {
1133 let source = "pragma solidity >=0.6.2 <0.9.0;\n";
1134 assert_eq!(
1135 parse_pragma(source),
1136 Some(PragmaConstraint::Range(
1137 SemVer {
1138 major: 0,
1139 minor: 6,
1140 patch: 2
1141 },
1142 SemVer {
1143 major: 0,
1144 minor: 9,
1145 patch: 0
1146 },
1147 ))
1148 );
1149 }
1150
1151 #[test]
1152 fn test_parse_pragma_none() {
1153 let source = "contract Foo {}\n";
1154 assert_eq!(parse_pragma(source), None);
1155 }
1156
1157 #[test]
1158 fn test_version_satisfies_exact() {
1159 let v = SemVer {
1160 major: 0,
1161 minor: 8,
1162 patch: 26,
1163 };
1164 assert!(version_satisfies(&v, &PragmaConstraint::Exact(v.clone())));
1165 assert!(!version_satisfies(
1166 &SemVer {
1167 major: 0,
1168 minor: 8,
1169 patch: 25
1170 },
1171 &PragmaConstraint::Exact(v)
1172 ));
1173 }
1174
1175 #[test]
1176 fn test_version_satisfies_caret() {
1177 let constraint = PragmaConstraint::Caret(SemVer {
1178 major: 0,
1179 minor: 8,
1180 patch: 0,
1181 });
1182 assert!(version_satisfies(
1183 &SemVer {
1184 major: 0,
1185 minor: 8,
1186 patch: 0
1187 },
1188 &constraint
1189 ));
1190 assert!(version_satisfies(
1191 &SemVer {
1192 major: 0,
1193 minor: 8,
1194 patch: 26
1195 },
1196 &constraint
1197 ));
1198 assert!(!version_satisfies(
1200 &SemVer {
1201 major: 0,
1202 minor: 9,
1203 patch: 0
1204 },
1205 &constraint
1206 ));
1207 assert!(!version_satisfies(
1209 &SemVer {
1210 major: 0,
1211 minor: 7,
1212 patch: 0
1213 },
1214 &constraint
1215 ));
1216 }
1217
1218 #[test]
1219 fn test_version_satisfies_gte() {
1220 let constraint = PragmaConstraint::Gte(SemVer {
1221 major: 0,
1222 minor: 8,
1223 patch: 0,
1224 });
1225 assert!(version_satisfies(
1226 &SemVer {
1227 major: 0,
1228 minor: 8,
1229 patch: 0
1230 },
1231 &constraint
1232 ));
1233 assert!(version_satisfies(
1234 &SemVer {
1235 major: 0,
1236 minor: 9,
1237 patch: 0
1238 },
1239 &constraint
1240 ));
1241 assert!(!version_satisfies(
1242 &SemVer {
1243 major: 0,
1244 minor: 7,
1245 patch: 0
1246 },
1247 &constraint
1248 ));
1249 }
1250
1251 #[test]
1252 fn test_version_satisfies_range() {
1253 let constraint = PragmaConstraint::Range(
1254 SemVer {
1255 major: 0,
1256 minor: 6,
1257 patch: 2,
1258 },
1259 SemVer {
1260 major: 0,
1261 minor: 9,
1262 patch: 0,
1263 },
1264 );
1265 assert!(version_satisfies(
1266 &SemVer {
1267 major: 0,
1268 minor: 6,
1269 patch: 2
1270 },
1271 &constraint
1272 ));
1273 assert!(version_satisfies(
1274 &SemVer {
1275 major: 0,
1276 minor: 8,
1277 patch: 26
1278 },
1279 &constraint
1280 ));
1281 assert!(!version_satisfies(
1283 &SemVer {
1284 major: 0,
1285 minor: 9,
1286 patch: 0
1287 },
1288 &constraint
1289 ));
1290 assert!(!version_satisfies(
1291 &SemVer {
1292 major: 0,
1293 minor: 6,
1294 patch: 1
1295 },
1296 &constraint
1297 ));
1298 }
1299
1300 #[test]
1301 fn test_find_matching_version() {
1302 let installed = vec![
1303 SemVer {
1304 major: 0,
1305 minor: 8,
1306 patch: 0,
1307 },
1308 SemVer {
1309 major: 0,
1310 minor: 8,
1311 patch: 20,
1312 },
1313 SemVer {
1314 major: 0,
1315 minor: 8,
1316 patch: 26,
1317 },
1318 SemVer {
1319 major: 0,
1320 minor: 8,
1321 patch: 33,
1322 },
1323 ];
1324 let constraint = PragmaConstraint::Caret(SemVer {
1326 major: 0,
1327 minor: 8,
1328 patch: 20,
1329 });
1330 let matched = find_matching_version(&constraint, &installed);
1331 assert_eq!(
1332 matched,
1333 Some(SemVer {
1334 major: 0,
1335 minor: 8,
1336 patch: 33
1337 })
1338 );
1339
1340 let constraint = PragmaConstraint::Exact(SemVer {
1342 major: 0,
1343 minor: 8,
1344 patch: 20,
1345 });
1346 let matched = find_matching_version(&constraint, &installed);
1347 assert_eq!(
1348 matched,
1349 Some(SemVer {
1350 major: 0,
1351 minor: 8,
1352 patch: 20
1353 })
1354 );
1355
1356 let constraint = PragmaConstraint::Exact(SemVer {
1358 major: 0,
1359 minor: 8,
1360 patch: 15,
1361 });
1362 let matched = find_matching_version(&constraint, &installed);
1363 assert_eq!(matched, None);
1364 }
1365
1366 #[test]
1367 fn test_list_installed_versions() {
1368 let versions = list_installed_versions();
1370 for w in versions.windows(2) {
1372 assert!(w[0] <= w[1]);
1373 }
1374 }
1375}