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 if let Some(c) = client {
900 c.log_message(
901 tower_lsp::lsp_types::MessageType::INFO,
902 format!(
903 "project index: discovered {} source files in {}",
904 source_files.len(),
905 config.root.display()
906 ),
907 )
908 .await;
909 }
910
911 let first_source = text_cache
914 .and_then(|tc| {
915 let uri = Url::from_file_path(&source_files[0]).ok()?;
916 tc.get(&uri.to_string()).map(|(_, c)| c.clone())
917 })
918 .or_else(|| std::fs::read_to_string(&source_files[0]).ok());
919 let solc_binary = resolve_solc_binary(config, first_source.as_deref(), client).await;
920 let remappings = resolve_remappings(config).await;
921
922 let input =
923 build_batch_standard_json_input_with_cache(&source_files, &remappings, config, text_cache);
924 let raw_output = run_solc(&solc_binary, &input, &config.root).await?;
925 Ok(normalize_solc_output(raw_output, Some(&config.root)))
926}
927
928#[cfg(test)]
929mod tests {
930 use super::*;
931
932 #[test]
933 fn test_normalize_solc_sources() {
934 let solc_output = json!({
935 "sources": {
936 "src/Foo.sol": {
937 "id": 0,
938 "ast": {
939 "nodeType": "SourceUnit",
940 "absolutePath": "src/Foo.sol",
941 "id": 100
942 }
943 },
944 "src/Bar.sol": {
945 "id": 1,
946 "ast": {
947 "nodeType": "SourceUnit",
948 "absolutePath": "src/Bar.sol",
949 "id": 200
950 }
951 }
952 },
953 "contracts": {},
954 "errors": []
955 });
956
957 let normalized = normalize_solc_output(solc_output, None);
958
959 let sources = normalized.get("sources").unwrap().as_object().unwrap();
961 assert_eq!(sources.len(), 2);
962
963 let foo = sources.get("src/Foo.sol").unwrap();
964 assert_eq!(foo.get("id").unwrap(), 0);
965 assert_eq!(
966 foo.get("ast")
967 .unwrap()
968 .get("nodeType")
969 .unwrap()
970 .as_str()
971 .unwrap(),
972 "SourceUnit"
973 );
974
975 let id_to_path = normalized
977 .get("source_id_to_path")
978 .unwrap()
979 .as_object()
980 .unwrap();
981 assert_eq!(id_to_path.len(), 2);
982 }
983
984 #[test]
985 fn test_normalize_solc_contracts() {
986 let solc_output = json!({
987 "sources": {},
988 "contracts": {
989 "src/Foo.sol": {
990 "Foo": {
991 "abi": [{"type": "function", "name": "bar"}],
992 "evm": {
993 "methodIdentifiers": {
994 "bar(uint256)": "abcd1234"
995 },
996 "gasEstimates": {
997 "external": {"bar(uint256)": "200"}
998 }
999 }
1000 }
1001 }
1002 },
1003 "errors": []
1004 });
1005
1006 let normalized = normalize_solc_output(solc_output, None);
1007
1008 let contracts = normalized.get("contracts").unwrap().as_object().unwrap();
1010 let foo_contracts = contracts.get("src/Foo.sol").unwrap().as_object().unwrap();
1011 let foo = foo_contracts.get("Foo").unwrap();
1012
1013 let method_ids = foo
1014 .get("evm")
1015 .unwrap()
1016 .get("methodIdentifiers")
1017 .unwrap()
1018 .as_object()
1019 .unwrap();
1020 assert_eq!(
1021 method_ids.get("bar(uint256)").unwrap().as_str().unwrap(),
1022 "abcd1234"
1023 );
1024 }
1025
1026 #[test]
1027 fn test_normalize_solc_errors_passthrough() {
1028 let solc_output = json!({
1029 "sources": {},
1030 "contracts": {},
1031 "errors": [{
1032 "sourceLocation": {"file": "src/Foo.sol", "start": 0, "end": 10},
1033 "type": "Warning",
1034 "component": "general",
1035 "severity": "warning",
1036 "errorCode": "2394",
1037 "message": "test warning",
1038 "formattedMessage": "Warning: test warning"
1039 }]
1040 });
1041
1042 let normalized = normalize_solc_output(solc_output, None);
1043
1044 let errors = normalized.get("errors").unwrap().as_array().unwrap();
1045 assert_eq!(errors.len(), 1);
1046 assert_eq!(
1047 errors[0].get("errorCode").unwrap().as_str().unwrap(),
1048 "2394"
1049 );
1050 }
1051
1052 #[test]
1053 fn test_normalize_empty_solc_output() {
1054 let solc_output = json!({
1055 "sources": {},
1056 "contracts": {}
1057 });
1058
1059 let normalized = normalize_solc_output(solc_output, None);
1060
1061 assert!(
1062 normalized
1063 .get("sources")
1064 .unwrap()
1065 .as_object()
1066 .unwrap()
1067 .is_empty()
1068 );
1069 assert!(
1070 normalized
1071 .get("contracts")
1072 .unwrap()
1073 .as_object()
1074 .unwrap()
1075 .is_empty()
1076 );
1077 assert_eq!(
1078 normalized.get("errors").unwrap().as_array().unwrap().len(),
1079 0
1080 );
1081 assert!(
1082 normalized
1083 .get("source_id_to_path")
1084 .unwrap()
1085 .as_object()
1086 .unwrap()
1087 .is_empty()
1088 );
1089 }
1090
1091 #[test]
1092 fn test_build_standard_json_input() {
1093 let config = FoundryConfig::default();
1094 let input = build_standard_json_input(
1095 "/path/to/Foo.sol",
1096 &[
1097 "ds-test/=lib/forge-std/lib/ds-test/src/".to_string(),
1098 "forge-std/=lib/forge-std/src/".to_string(),
1099 ],
1100 &config,
1101 );
1102
1103 let sources = input.get("sources").unwrap().as_object().unwrap();
1104 assert!(sources.contains_key("/path/to/Foo.sol"));
1105
1106 let settings = input.get("settings").unwrap();
1107 let remappings = settings.get("remappings").unwrap().as_array().unwrap();
1108 assert_eq!(remappings.len(), 2);
1109
1110 let output_sel = settings.get("outputSelection").unwrap();
1111 assert!(output_sel.get("*").is_some());
1112
1113 assert!(settings.get("optimizer").is_none());
1115 assert!(settings.get("viaIR").is_none());
1116 assert!(settings.get("evmVersion").is_none());
1117
1118 let outputs = settings["outputSelection"]["*"]["*"].as_array().unwrap();
1120 let output_names: Vec<&str> = outputs.iter().map(|v| v.as_str().unwrap()).collect();
1121 assert!(output_names.contains(&"evm.gasEstimates"));
1122 assert!(output_names.contains(&"abi"));
1123 assert!(output_names.contains(&"devdoc"));
1124 assert!(output_names.contains(&"userdoc"));
1125 assert!(output_names.contains(&"evm.methodIdentifiers"));
1126 }
1127
1128 #[test]
1129 fn test_build_standard_json_input_with_config() {
1130 let config = FoundryConfig {
1131 optimizer: true,
1132 optimizer_runs: 9999999,
1133 via_ir: true,
1134 evm_version: Some("osaka".to_string()),
1135 ..Default::default()
1136 };
1137 let input = build_standard_json_input("/path/to/Foo.sol", &[], &config);
1138
1139 let settings = input.get("settings").unwrap();
1140
1141 assert!(settings.get("optimizer").is_none());
1143
1144 assert!(settings.get("viaIR").unwrap().as_bool().unwrap());
1146
1147 let outputs = settings["outputSelection"]["*"]["*"].as_array().unwrap();
1149 let output_names: Vec<&str> = outputs.iter().map(|v| v.as_str().unwrap()).collect();
1150 assert!(!output_names.contains(&"evm.gasEstimates"));
1151
1152 assert_eq!(
1154 settings.get("evmVersion").unwrap().as_str().unwrap(),
1155 "osaka"
1156 );
1157 }
1158
1159 #[tokio::test]
1160 async fn test_resolve_solc_binary_default() {
1161 let config = FoundryConfig::default();
1162 let binary = resolve_solc_binary(&config, None, None).await;
1163 assert_eq!(binary, PathBuf::from("solc"));
1164 }
1165
1166 #[test]
1167 fn test_parse_pragma_exact() {
1168 let source = "// SPDX\npragma solidity 0.8.26;\n";
1169 assert_eq!(
1170 parse_pragma(source),
1171 Some(PragmaConstraint::Exact(SemVer {
1172 major: 0,
1173 minor: 8,
1174 patch: 26
1175 }))
1176 );
1177 }
1178
1179 #[test]
1180 fn test_parse_pragma_caret() {
1181 let source = "pragma solidity ^0.8.0;\n";
1182 assert_eq!(
1183 parse_pragma(source),
1184 Some(PragmaConstraint::Caret(SemVer {
1185 major: 0,
1186 minor: 8,
1187 patch: 0
1188 }))
1189 );
1190 }
1191
1192 #[test]
1193 fn test_parse_pragma_gte() {
1194 let source = "pragma solidity >=0.8.0;\n";
1195 assert_eq!(
1196 parse_pragma(source),
1197 Some(PragmaConstraint::Gte(SemVer {
1198 major: 0,
1199 minor: 8,
1200 patch: 0
1201 }))
1202 );
1203 }
1204
1205 #[test]
1206 fn test_parse_pragma_range() {
1207 let source = "pragma solidity >=0.6.2 <0.9.0;\n";
1208 assert_eq!(
1209 parse_pragma(source),
1210 Some(PragmaConstraint::Range(
1211 SemVer {
1212 major: 0,
1213 minor: 6,
1214 patch: 2
1215 },
1216 SemVer {
1217 major: 0,
1218 minor: 9,
1219 patch: 0
1220 },
1221 ))
1222 );
1223 }
1224
1225 #[test]
1226 fn test_parse_pragma_none() {
1227 let source = "contract Foo {}\n";
1228 assert_eq!(parse_pragma(source), None);
1229 }
1230
1231 #[test]
1232 fn test_version_satisfies_exact() {
1233 let v = SemVer {
1234 major: 0,
1235 minor: 8,
1236 patch: 26,
1237 };
1238 assert!(version_satisfies(&v, &PragmaConstraint::Exact(v.clone())));
1239 assert!(!version_satisfies(
1240 &SemVer {
1241 major: 0,
1242 minor: 8,
1243 patch: 25
1244 },
1245 &PragmaConstraint::Exact(v)
1246 ));
1247 }
1248
1249 #[test]
1250 fn test_version_satisfies_caret() {
1251 let constraint = PragmaConstraint::Caret(SemVer {
1252 major: 0,
1253 minor: 8,
1254 patch: 0,
1255 });
1256 assert!(version_satisfies(
1257 &SemVer {
1258 major: 0,
1259 minor: 8,
1260 patch: 0
1261 },
1262 &constraint
1263 ));
1264 assert!(version_satisfies(
1265 &SemVer {
1266 major: 0,
1267 minor: 8,
1268 patch: 26
1269 },
1270 &constraint
1271 ));
1272 assert!(!version_satisfies(
1274 &SemVer {
1275 major: 0,
1276 minor: 9,
1277 patch: 0
1278 },
1279 &constraint
1280 ));
1281 assert!(!version_satisfies(
1283 &SemVer {
1284 major: 0,
1285 minor: 7,
1286 patch: 0
1287 },
1288 &constraint
1289 ));
1290 }
1291
1292 #[test]
1293 fn test_version_satisfies_gte() {
1294 let constraint = PragmaConstraint::Gte(SemVer {
1295 major: 0,
1296 minor: 8,
1297 patch: 0,
1298 });
1299 assert!(version_satisfies(
1300 &SemVer {
1301 major: 0,
1302 minor: 8,
1303 patch: 0
1304 },
1305 &constraint
1306 ));
1307 assert!(version_satisfies(
1308 &SemVer {
1309 major: 0,
1310 minor: 9,
1311 patch: 0
1312 },
1313 &constraint
1314 ));
1315 assert!(!version_satisfies(
1316 &SemVer {
1317 major: 0,
1318 minor: 7,
1319 patch: 0
1320 },
1321 &constraint
1322 ));
1323 }
1324
1325 #[test]
1326 fn test_version_satisfies_range() {
1327 let constraint = PragmaConstraint::Range(
1328 SemVer {
1329 major: 0,
1330 minor: 6,
1331 patch: 2,
1332 },
1333 SemVer {
1334 major: 0,
1335 minor: 9,
1336 patch: 0,
1337 },
1338 );
1339 assert!(version_satisfies(
1340 &SemVer {
1341 major: 0,
1342 minor: 6,
1343 patch: 2
1344 },
1345 &constraint
1346 ));
1347 assert!(version_satisfies(
1348 &SemVer {
1349 major: 0,
1350 minor: 8,
1351 patch: 26
1352 },
1353 &constraint
1354 ));
1355 assert!(!version_satisfies(
1357 &SemVer {
1358 major: 0,
1359 minor: 9,
1360 patch: 0
1361 },
1362 &constraint
1363 ));
1364 assert!(!version_satisfies(
1365 &SemVer {
1366 major: 0,
1367 minor: 6,
1368 patch: 1
1369 },
1370 &constraint
1371 ));
1372 }
1373
1374 #[test]
1375 fn test_find_matching_version() {
1376 let installed = vec![
1377 SemVer {
1378 major: 0,
1379 minor: 8,
1380 patch: 0,
1381 },
1382 SemVer {
1383 major: 0,
1384 minor: 8,
1385 patch: 20,
1386 },
1387 SemVer {
1388 major: 0,
1389 minor: 8,
1390 patch: 26,
1391 },
1392 SemVer {
1393 major: 0,
1394 minor: 8,
1395 patch: 33,
1396 },
1397 ];
1398 let constraint = PragmaConstraint::Caret(SemVer {
1400 major: 0,
1401 minor: 8,
1402 patch: 20,
1403 });
1404 let matched = find_matching_version(&constraint, &installed);
1405 assert_eq!(
1406 matched,
1407 Some(SemVer {
1408 major: 0,
1409 minor: 8,
1410 patch: 33
1411 })
1412 );
1413
1414 let constraint = PragmaConstraint::Exact(SemVer {
1416 major: 0,
1417 minor: 8,
1418 patch: 20,
1419 });
1420 let matched = find_matching_version(&constraint, &installed);
1421 assert_eq!(
1422 matched,
1423 Some(SemVer {
1424 major: 0,
1425 minor: 8,
1426 patch: 20
1427 })
1428 );
1429
1430 let constraint = PragmaConstraint::Exact(SemVer {
1432 major: 0,
1433 minor: 8,
1434 patch: 15,
1435 });
1436 let matched = find_matching_version(&constraint, &installed);
1437 assert_eq!(matched, None);
1438 }
1439
1440 #[test]
1441 fn test_list_installed_versions() {
1442 let versions = list_installed_versions();
1444 for w in versions.windows(2) {
1446 assert!(w[0] <= w[1]);
1447 }
1448 }
1449}