Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

feat(solc): color when formatting Error and OutputDiagnostics #2368

Merged
merged 5 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 227 additions & 68 deletions ethers-solc/src/artifacts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
use std::{
collections::{BTreeMap, HashSet},
fmt, fs,
ops::Range,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use tracing::warn;
use yansi::Paint;
use yansi::{Color, Paint, Style};

pub mod ast;
pub use ast::*;
Expand Down Expand Up @@ -1837,31 +1838,211 @@ pub struct Error {
pub formatted_message: Option<String>,
}

/// Tries to mimic Solidity's own error formatting.
///
/// <https://github.com/ethereum/solidity/blob/a297a687261a1c634551b1dac0e36d4573c19afe/liblangutil/SourceReferenceFormatter.cpp#L105>
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !Paint::is_enabled() {
let msg = self.formatted_message.as_ref().unwrap_or(&self.message);
self.fmt_severity(f)?;
f.write_str(": ")?;
return f.write_str(msg)
}

// Error (XXXX): Error Message
styled(f, self.severity.color().style().bold(), |f| self.fmt_severity(f))?;
fmt_msg(f, &self.message)?;

if let Some(msg) = &self.formatted_message {
match self.severity {
Severity::Error => {
if let Some(code) = self.error_code {
Paint::red(format!("error[{code}]: ")).fmt(f)?;
}
Paint::red(msg).fmt(f)
}
Severity::Warning | Severity::Info => {
if let Some(code) = self.error_code {
Paint::yellow(format!("warning[{code}]: ")).fmt(f)?;
}
Paint::yellow(msg).fmt(f)
let mut lines = msg.lines();

// skip first line, it should be similar to the error message we wrote above
lines.next();

// format the main source location
fmt_source_location(f, &mut lines)?;

// format remaining lines as secondary locations
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 {
/// The style of the diagnostic severity.
pub fn error_style(&self) -> Style {
self.severity.color().style().bold()
}

/// The style of the diagnostic message.
pub fn message_style() -> Style {
Color::White.style().bold()
}

/// The style of the secondary source location.
pub fn secondary_style() -> Style {
Color::Cyan.style().bold()
}

/// The style of the source location highlight.
pub fn highlight_style() -> Style {
Color::Yellow.style()
}

/// The style of the diagnostics.
pub fn diag_style() -> Style {
Color::Yellow.style().bold()
}

/// The style of the source location frame.
pub fn frame_style() -> Style {
Color::Blue.style()
}

/// Formats the diagnostic severity:
///
/// ```text
/// Error (XXXX)
/// ```
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(())
}
}

/// Calls `fun` in between [`Style::fmt_prefix`] and [`Style::fmt_suffix`].
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)
}

/// Formats the diagnostic message.
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())
})
}

/// Colors a Solidity source location:
///
/// ```text
/// --> /home/user/contract.sol:420:69:
/// |
/// 420 | bad_code()
/// | ^
/// ```
fn fmt_source_location(f: &mut fmt::Formatter, lines: &mut std::str::Lines) -> fmt::Result {
// --> source
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 {
self.severity.fmt(f)?;
writeln!(f, ": {}", self.message)
f.write_str(line)?;
}
}

// get the next 3 lines
// FIXME: Somehow do this without allocating
let next_3 = lines.take(3).collect::<Vec<_>>();
let [line1, line2, line3] = next_3[..] else {
for line in next_3 {
f.write_str("\n")?;
f.write_str(line)?;
}
return Ok(())
};

// line 1, just a frame
fmt_framed_location(f, line1, None)?;

// line 2, frame and code; highlight the text based on line 3's carets
let hl_start = line3.find('^');
let highlight = hl_start.map(|start| {
let end = if line3.contains("^ (") {
// highlight the entire line because of "spans across multiple lines" diagnostic
line2.len()
} else if let Some(carets) = line3[start..].find(|c: char| c != '^') {
// highlight the text that the carets point to
start + carets
} else {
// the carets span the entire third line
line3.len()
}
// bound in case carets span longer than the code they point to
.min(line2.len());
(start.min(end)..end, Error::highlight_style())
});
fmt_framed_location(f, line2, highlight)?;

// line 3, frame and maybe highlight, this time till the end unconditionally
let highlight = hl_start.map(|i| (i..line3.len(), Error::diag_style()));
fmt_framed_location(f, line3, highlight)
}

/// Colors a single Solidity framed source location line. Part of [`fmt_source_location`].
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 the potential frame is not just whitespace or numbers, don't color it
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.clone();
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)
}
}

#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Default)]
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
#[default]
Error,
Expand All @@ -1871,25 +2052,7 @@ pub enum Severity {

impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Severity::Error => Paint::red("Error").fmt(f),
Severity::Warning => Paint::yellow("Warning").fmt(f),
Severity::Info => f.write_str("Info"),
}
}
}

impl Severity {
pub fn is_error(&self) -> bool {
matches!(self, Severity::Error)
}

pub fn is_warning(&self) -> bool {
matches!(self, Severity::Warning)
}

pub fn is_info(&self) -> bool {
matches!(self, Severity::Info)
f.write_str(self.as_str())
}
}

Expand All @@ -1898,50 +2061,46 @@ impl FromStr for Severity {

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"error" => Ok(Severity::Error),
"warning" => Ok(Severity::Warning),
"info" => Ok(Severity::Info),
"Error" | "error" => Ok(Self::Error),
"Warning" | "warning" => Ok(Self::Warning),
"Info" | "info" => Ok(Self::Info),
s => Err(format!("Invalid severity: {s}")),
}
}
}

impl Serialize for Severity {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Severity::Error => serializer.serialize_str("error"),
Severity::Warning => serializer.serialize_str("warning"),
Severity::Info => serializer.serialize_str("info"),
}
impl Severity {
/// Returns `true` if the severity is `Error`.
pub const fn is_error(&self) -> bool {
matches!(self, Self::Error)
}
}

impl<'de> Deserialize<'de> for Severity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct SeverityVisitor;

impl<'de> Visitor<'de> for SeverityVisitor {
type Value = Severity;
/// Returns `true` if the severity is `Warning`.
pub const fn is_warning(&self) -> bool {
matches!(self, Self::Warning)
}

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "severity string")
}
/// Returns `true` if the severity is `Info`.
pub const fn is_info(&self) -> bool {
matches!(self, Self::Info)
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
value.parse().map_err(serde::de::Error::custom)
}
/// Returns the string representation of the severity.
pub const fn as_str(&self) -> &'static str {
match self {
Self::Error => "Error",
Self::Warning => "Warning",
Self::Info => "Info",
}
}

deserializer.deserialize_str(SeverityVisitor)
/// Returns the color to format the severity with.
pub const fn color(&self) -> Color {
match self {
Self::Error => Color::Red,
Self::Warning => Color::Yellow,
Self::Info => Color::White,
}
}
}

Expand Down
Loading