Skip to content

Commit 71a4990

Browse files
authored
feat: better error messages for incompatible versions (#200)
1 parent 3a9139a commit 71a4990

File tree

7 files changed

+112
-39
lines changed

7 files changed

+112
-39
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,5 @@ yansi = "1.0"
6262
# async
6363
futures-util = "0.3"
6464
tokio = { version = "1.35", features = ["rt-multi-thread"] }
65+
66+
snapbox = "0.6.9"

crates/compilers/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ fd-lock = "4.0.0"
6161
tokio = { version = "1.35", features = ["rt-multi-thread", "macros"] }
6262
reqwest = "0.12"
6363
tempfile = "3.9"
64+
snapbox.workspace = true
6465
foundry-compilers-core = { workspace = true, features = ["test-utils"] }
6566

6667
[features]

crates/compilers/src/resolver/mod.rs

Lines changed: 103 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ use std::{
6464
io,
6565
path::{Path, PathBuf},
6666
};
67+
use yansi::{Color, Paint};
6768

6869
pub mod parse;
6970
mod tree;
@@ -547,37 +548,92 @@ impl<L: Language, D: ParsedSource<Language = L>> Graph<D> {
547548
fn format_imports_list<W: std::fmt::Write>(
548549
&self,
549550
idx: usize,
551+
incompatible: HashSet<usize>,
550552
f: &mut W,
551553
) -> std::result::Result<(), std::fmt::Error> {
552-
let node = self.node(idx);
553-
write!(f, "{} ", utils::source_name(&node.path, &self.root).display())?;
554-
if let Some(req) = node.data.version_req() {
555-
write!(f, "{req}")?;
556-
}
554+
let format_node = |idx, f: &mut W| {
555+
let node = self.node(idx);
556+
let color = if incompatible.contains(&idx) { Color::Red } else { Color::White };
557+
558+
let mut line = utils::source_name(&node.path, &self.root).display().to_string();
559+
if let Some(req) = node.data.version_req() {
560+
line.push_str(&format!(" {req}"));
561+
}
562+
563+
write!(f, "{}", line.paint(color))
564+
};
565+
format_node(idx, f)?;
557566
write!(f, " imports:")?;
558567
for dep in self.node_ids(idx).skip(1) {
559-
let dep = self.node(dep);
560-
write!(f, "\n {} ", utils::source_name(&dep.path, &self.root).display())?;
561-
if let Some(req) = dep.data.version_req() {
562-
write!(f, "{req}")?;
563-
}
568+
write!(f, "\n ")?;
569+
format_node(dep, f)?;
564570
}
565571

566572
Ok(())
567573
}
568574

569-
/// Filters incompatible versions from the `candidates`.
570-
fn retain_compatible_versions(&self, idx: usize, candidates: &mut Vec<&CompilerVersion>) {
571-
let nodes: HashSet<_> = self.node_ids(idx).collect();
572-
for node in nodes {
573-
if let Some(req) = &self.node(node).data.version_req() {
575+
/// Filters incompatible versions from the `candidates`. It iterates over node imports and in
576+
/// case if there is no compatible version it returns the latest seen node id.
577+
fn retain_compatible_versions(
578+
&self,
579+
idx: usize,
580+
candidates: &mut Vec<&CompilerVersion>,
581+
offline: bool,
582+
) -> Result<(), String> {
583+
let mut all_versions = candidates.clone();
584+
585+
let nodes: Vec<_> = self.node_ids(idx).collect();
586+
let mut failed_node = None;
587+
for node in nodes.iter() {
588+
if let Some(req) = self.node(*node).data.version_req() {
574589
candidates.retain(|v| req.matches(v.as_ref()));
590+
591+
if candidates.is_empty() {
592+
failed_node = Some(*node);
593+
break;
594+
}
595+
}
596+
}
597+
598+
let Some(failed_node_idx) = failed_node else {
599+
// everything is fine
600+
return Ok(());
601+
};
602+
603+
// This now keeps data for the node which were the last one before we had no candidates
604+
// left. It means that there
605+
let failed_node = self.node(failed_node_idx);
606+
607+
if let Err(version_err) = failed_node.check_available_version(&all_versions, offline) {
608+
// check if the version is even valid
609+
let f = utils::source_name(&failed_node.path, &self.root).display();
610+
return Err(
611+
format!("Encountered invalid solc version in {f}: {version_err}").to_string()
612+
);
613+
} else {
614+
// if the node requirement makes sense, it means that there is at least one node
615+
// which requirement conflicts with it
616+
617+
// retain only versions compatible with the `failed_node`
618+
if let Some(req) = failed_node.data.version_req() {
619+
all_versions.retain(|v| req.matches(v.as_ref()));
575620
}
576-
if candidates.is_empty() {
577-
// nothing to filter anymore
578-
return;
621+
622+
// iterate over all the nodes once again and find the one incompatible
623+
for node in &nodes {
624+
if self.node(*node).check_available_version(&all_versions, offline).is_err() {
625+
let mut msg = "Found incompatible versions:\n".white().to_string();
626+
627+
self.format_imports_list(idx, [*node, failed_node_idx].into(), &mut msg)
628+
.unwrap();
629+
return Err(msg);
630+
}
579631
}
580632
}
633+
634+
let mut msg = "Found incompatible versions:\n".white().to_string();
635+
self.format_imports_list(idx, nodes.into_iter().collect(), &mut msg).unwrap();
636+
Err(msg)
581637
}
582638

583639
fn input_nodes_by_language(&self) -> HashMap<D::Language, Vec<usize>> {
@@ -619,8 +675,6 @@ impl<L: Language, D: ParsedSource<Language = L>> Graph<D> {
619675
// exit on first error, instead gather all the errors and return a bundled
620676
// error message instead
621677
let mut errors = Vec::new();
622-
// we also don't want duplicate error diagnostic
623-
let mut erroneous_nodes = HashSet::with_capacity(self.edges.num_input_files);
624678

625679
// the sorted list of all versions
626680
let all_versions = if offline {
@@ -649,23 +703,8 @@ impl<L: Language, D: ParsedSource<Language = L>> Graph<D> {
649703
let mut candidates = all_versions.iter().collect::<Vec<_>>();
650704
// remove all incompatible versions from the candidates list by checking the node
651705
// and all its imports
652-
self.retain_compatible_versions(idx, &mut candidates);
653-
654-
if candidates.is_empty() && !erroneous_nodes.contains(&idx) {
655-
// check if the version is even valid
656-
let node = self.node(idx);
657-
if let Err(version_err) = node.check_available_version(&all_versions, offline) {
658-
let f = utils::source_name(&node.path, &self.root).display();
659-
errors.push(format!(
660-
"Encountered invalid solc version in {f}: {version_err}"
661-
));
662-
} else {
663-
let mut msg = String::new();
664-
self.format_imports_list(idx, &mut msg).unwrap();
665-
errors.push(format!("Found incompatible versions:\n{msg}"));
666-
}
667-
668-
erroneous_nodes.insert(idx);
706+
if let Err(err) = self.retain_compatible_versions(idx, &mut candidates, offline) {
707+
errors.push(err);
669708
} else {
670709
// found viable candidates, pick the most recent version that's already
671710
// installed
@@ -878,7 +917,7 @@ impl<D: ParsedSource> Node<D> {
878917
/// 0.8.20, if the highest available version is `0.8.19`
879918
fn check_available_version(
880919
&self,
881-
all_versions: &[CompilerVersion],
920+
all_versions: &[&CompilerVersion],
882921
offline: bool,
883922
) -> std::result::Result<(), SourceVersionError> {
884923
let Some(req) = self.data.version_req() else { return Ok(()) };
@@ -1013,6 +1052,32 @@ src/Dapp.t.sol >=0.6.6
10131052
);
10141053
}
10151054

1055+
#[test]
1056+
#[cfg(feature = "svm-solc")]
1057+
fn test_print_unresolved() {
1058+
let root =
1059+
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/incompatible-pragmas");
1060+
let paths = ProjectPathsConfig::dapptools(&root).unwrap();
1061+
let graph = Graph::<SolData>::resolve(&paths).unwrap();
1062+
let Err(SolcError::Message(err)) = graph.get_input_node_versions(
1063+
false,
1064+
&Default::default(),
1065+
&crate::solc::SolcCompiler::AutoDetect,
1066+
) else {
1067+
panic!("expected error");
1068+
};
1069+
1070+
snapbox::assert_data_eq!(
1071+
err,
1072+
snapbox::str![[r#"
1073+
Found incompatible versions:
1074+
src/A.sol =0.8.25 imports:
1075+
src/B.sol
1076+
src/C.sol =0.7.0
1077+
"#]]
1078+
);
1079+
}
1080+
10161081
#[cfg(target_os = "linux")]
10171082
#[test]
10181083
fn can_read_different_case() {

crates/core/src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::{
55
};
66
use thiserror::Error;
77

8-
pub type Result<T> = std::result::Result<T, SolcError>;
8+
pub type Result<T, E = SolcError> = std::result::Result<T, E>;
99

1010
#[allow(unused_macros)]
1111
#[macro_export]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pragma solidity 0.8.25;
2+
3+
import "./B.sol";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import "./C.sol";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pragma solidity 0.7.0;

0 commit comments

Comments
 (0)