use super::serde_helpers;
use serde::{Deserialize, Serialize};
use std::{fmt, ops::Range, str::FromStr};
use yansi::{Color, Paint, Style};
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub struct SourceLocation {
pub file: String,
pub start: i32,
pub end: i32,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub struct SecondarySourceLocation {
pub file: Option<String>,
pub start: Option<i32>,
pub end: Option<i32>,
pub message: Option<String>,
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
#[default]
Error,
Warning,
Info,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Severity {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Error" | "error" => Ok(Self::Error),
"Warning" | "warning" => Ok(Self::Warning),
"Info" | "info" => Ok(Self::Info),
s => Err(format!("Invalid severity: {s}")),
}
}
}
impl Severity {
pub const fn is_error(&self) -> bool {
matches!(self, Self::Error)
}
pub const fn is_warning(&self) -> bool {
matches!(self, Self::Warning)
}
pub const fn is_info(&self) -> bool {
matches!(self, Self::Info)
}
pub const fn as_str(&self) -> &'static str {
match self {
Self::Error => "Error",
Self::Warning => "Warning",
Self::Info => "Info",
}
}
pub const fn color(&self) -> Color {
match self {
Self::Error => Color::Red,
Self::Warning => Color::Yellow,
Self::Info => Color::White,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct Error {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_location: Option<SourceLocation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secondary_source_locations: Vec<SecondarySourceLocation>,
pub r#type: String,
pub component: String,
pub severity: Severity,
#[serde(default, with = "serde_helpers::display_from_str_opt")]
pub error_code: Option<u64>,
pub message: String,
pub formatted_message: Option<String>,
}
impl Error {
pub const fn is_error(&self) -> bool {
self.severity.is_error()
}
pub const fn is_warning(&self) -> bool {
self.severity.is_warning()
}
pub const fn is_info(&self) -> bool {
self.severity.is_info()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut short_msg = self.message.trim();
let fmtd_msg = self.formatted_message.as_deref().unwrap_or("");
if !Paint::is_enabled() || !fmtd_msg.contains(" | ") {
return f.write_str(self.formatted_message.as_deref().unwrap_or(&self.message));
}
if short_msg.is_empty() {
if let Some(first_line) = fmtd_msg.lines().next() {
if let Some((_, s)) = first_line.split_once(':') {
short_msg = s.trim_start();
} else {
short_msg = first_line;
}
}
}
styled(f, self.severity.color().style().bold(), |f| self.fmt_severity(f))?;
fmt_msg(f, short_msg)?;
let mut lines = fmtd_msg.lines();
let _ = lines.next();
fmt_source_location(f, &mut lines)?;
while let Some(line) = lines.next() {
f.write_str("\n")?;
if let Some((note, msg)) = line.split_once(':') {
styled(f, Self::secondary_style(), |f| f.write_str(note))?;
fmt_msg(f, msg)?;
} else {
f.write_str(line)?;
}
fmt_source_location(f, &mut lines)?;
}
Ok(())
}
}
impl Error {
pub fn error_style(&self) -> Style {
self.severity.color().style().bold()
}
pub fn message_style() -> Style {
Color::White.style().bold()
}
pub fn secondary_style() -> Style {
Color::Cyan.style().bold()
}
pub fn highlight_style() -> Style {
Color::Yellow.style()
}
pub fn diag_style() -> Style {
Color::Yellow.style().bold()
}
pub fn frame_style() -> Style {
Color::Blue.style()
}
fn fmt_severity(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.severity.as_str())?;
if let Some(code) = self.error_code {
write!(f, " ({code})")?;
}
Ok(())
}
}
fn styled<F>(f: &mut fmt::Formatter<'_>, style: Style, fun: F) -> fmt::Result
where
F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result,
{
style.fmt_prefix(f)?;
fun(f)?;
style.fmt_suffix(f)
}
fn fmt_msg(f: &mut fmt::Formatter<'_>, msg: &str) -> fmt::Result {
styled(f, Error::message_style(), |f| {
f.write_str(": ")?;
f.write_str(msg.trim_start())
})
}
fn fmt_source_location(f: &mut fmt::Formatter<'_>, lines: &mut std::str::Lines<'_>) -> fmt::Result {
if let Some(line) = lines.next() {
f.write_str("\n")?;
let arrow = "-->";
if let Some((left, loc)) = line.split_once(arrow) {
f.write_str(left)?;
styled(f, Error::frame_style(), |f| f.write_str(arrow))?;
f.write_str(loc)?;
} else {
f.write_str(line)?;
}
}
let Some(line1) = lines.next() else {
return Ok(());
};
let Some(line2) = lines.next() else {
f.write_str("\n")?;
f.write_str(line1)?;
return Ok(());
};
let Some(line3) = lines.next() else {
f.write_str("\n")?;
f.write_str(line1)?;
f.write_str("\n")?;
f.write_str(line2)?;
return Ok(());
};
fmt_framed_location(f, line1, None)?;
let hl_start = line3.find('^');
let highlight = hl_start.map(|start| {
let end = if line3.contains("^ (") {
line2.len()
} else if let Some(carets) = line3[start..].find(|c: char| c != '^') {
start + carets
} else {
line3.len()
}
.min(line2.len());
(start.min(end)..end, Error::highlight_style())
});
fmt_framed_location(f, line2, highlight)?;
let highlight = hl_start.map(|i| (i..line3.len(), Error::diag_style()));
fmt_framed_location(f, line3, highlight)
}
fn fmt_framed_location(
f: &mut fmt::Formatter<'_>,
line: &str,
highlight: Option<(Range<usize>, Style)>,
) -> fmt::Result {
f.write_str("\n")?;
if let Some((space_or_line_number, rest)) = line.split_once('|') {
if !space_or_line_number.chars().all(|c| c.is_whitespace() || c.is_numeric()) {
return f.write_str(line);
}
styled(f, Error::frame_style(), |f| {
f.write_str(space_or_line_number)?;
f.write_str("|")
})?;
if let Some((range, style)) = highlight {
let Range { start, end } = range;
if !line.is_char_boundary(start) || !line.is_char_boundary(end) {
f.write_str(rest)
} else {
let rest_start = line.len() - rest.len();
f.write_str(&line[rest_start..start])?;
styled(f, style, |f| f.write_str(&line[range]))?;
f.write_str(&line[end..])
}
} else {
f.write_str(rest)
}
} else {
f.write_str(line)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fmt_unicode() {
let msg = "Invalid character in string. If you are trying to use Unicode characters, use a unicode\"...\" string literal.";
let e = Error {
source_location: Some(SourceLocation { file: "test/Counter.t.sol".into(), start: 418, end: 462 }),
secondary_source_locations: vec![],
r#type: "ParserError".into(),
component: "general".into(),
severity: Severity::Error,
error_code: Some(8936),
message: msg.into(),
formatted_message: Some("ParserError: Invalid character in string. If you are trying to use Unicode characters, use a unicode\"...\" string literal.\n --> test/Counter.t.sol:17:21:\n |\n17 | console.log(\"1. ownership set correctly as governance: ✓\");\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n".into()),
};
let s = e.to_string();
eprintln!("{s}");
assert!(s.contains(msg), "\n{s}");
}
#[test]
fn only_formatted() {
let e = Error {
source_location: Some(SourceLocation { file: "test/Counter.t.sol".into(), start: 418, end: 462 }),
secondary_source_locations: vec![],
r#type: "ParserError".into(),
component: "general".into(),
severity: Severity::Error,
error_code: Some(8936),
message: String::new(),
formatted_message: Some("ParserError: Invalid character in string. If you are trying to use Unicode characters, use a unicode\"...\" string literal.\n --> test/Counter.t.sol:17:21:\n |\n17 | console.log(\"1. ownership set correctly as governance: ✓\");\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n".into()),
};
let s = e.to_string();
eprintln!("{s}");
assert!(s.contains("Invalid character in string"), "\n{s}");
}
#[test]
fn solc_0_7() {
let output = r#"{"errors":[{"component":"general","errorCode":"6594","formattedMessage":"test/Counter.t.sol:7:1: TypeError: Contract \"CounterTest\" does not use ABI coder v2 but wants to inherit from a contract which uses types that require it. Use \"pragma abicoder v2;\" for the inheriting contract as well to enable the feature.\ncontract CounterTest is Test {\n^ (Relevant source part starts here and spans across multiple lines).\nlib/forge-std/src/StdInvariant.sol:72:5: Type only supported by ABIEncoderV2\n function excludeArtifacts() public view returns (string[] memory excludedArtifacts_) {\n ^ (Relevant source part starts here and spans across multiple lines).\nlib/forge-std/src/StdInvariant.sol:84:5: Type only supported by ABIEncoderV2\n function targetArtifacts() public view returns (string[] memory targetedArtifacts_) {\n ^ (Relevant source part starts here and spans across multiple lines).\nlib/forge-std/src/StdInvariant.sol:88:5: Type only supported by ABIEncoderV2\n function targetArtifactSelectors() public view returns (FuzzSelector[] memory targetedArtifactSelectors_) {\n ^ (Relevant source part starts here and spans across multiple lines).\nlib/forge-std/src/StdInvariant.sol:96:5: Type only supported by ABIEncoderV2\n function targetSelectors() public view returns (FuzzSelector[] memory targetedSelectors_) {\n ^ (Relevant source part starts here and spans across multiple lines).\nlib/forge-std/src/StdInvariant.sol:104:5: Type only supported by ABIEncoderV2\n function targetInterfaces() public view returns (FuzzInterface[] memory targetedInterfaces_) {\n ^ (Relevant source part starts here and spans across multiple lines).\n","message":"Contract \"CounterTest\" does not use ABI coder v2 but wants to inherit from a contract which uses types that require it. Use \"pragma abicoder v2;\" for the inheriting contract as well to enable the feature.","secondarySourceLocations":[{"end":2298,"file":"lib/forge-std/src/StdInvariant.sol","message":"Type only supported by ABIEncoderV2","start":2157},{"end":2732,"file":"lib/forge-std/src/StdInvariant.sol","message":"Type only supported by ABIEncoderV2","start":2592},{"end":2916,"file":"lib/forge-std/src/StdInvariant.sol","message":"Type only supported by ABIEncoderV2","start":2738},{"end":3215,"file":"lib/forge-std/src/StdInvariant.sol","message":"Type only supported by ABIEncoderV2","start":3069},{"end":3511,"file":"lib/forge-std/src/StdInvariant.sol","message":"Type only supported by ABIEncoderV2","start":3360}],"severity":"error","sourceLocation":{"end":558,"file":"test/Counter.t.sol","start":157},"type":"TypeError"}],"sources":{}}"#;
let crate::CompilerOutput { errors, .. } = serde_json::from_str(output).unwrap();
assert_eq!(errors.len(), 1);
let s = errors[0].to_string();
eprintln!("{s}");
assert!(s.contains("test/Counter.t.sol:7:1"), "{s}");
assert!(s.contains("ABI coder v2"), "{s}");
}
}