foundry_compilers_artifacts_solc/
error.rs

1use super::serde_helpers;
2use serde::{Deserialize, Serialize};
3use std::{fmt, ops::Range, str::FromStr};
4use yansi::{Color, Style};
5
6const ARROW: &str = "-->";
7
8#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct SourceLocation {
10    pub file: String,
11    pub start: i32,
12    pub end: i32,
13}
14
15#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub struct SecondarySourceLocation {
17    pub file: Option<String>,
18    pub start: Option<i32>,
19    pub end: Option<i32>,
20    pub message: Option<String>,
21}
22
23/// The severity of the error.
24#[derive(
25    Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
26)]
27#[serde(rename_all = "lowercase")]
28pub enum Severity {
29    /// Solc `Error`
30    #[default]
31    Error,
32    /// Solc `Warning`
33    Warning,
34    /// Solc `Info`
35    Info,
36}
37
38impl fmt::Display for Severity {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        f.write_str(self.as_str())
41    }
42}
43
44impl FromStr for Severity {
45    type Err = String;
46
47    fn from_str(s: &str) -> Result<Self, Self::Err> {
48        match s {
49            "Error" | "error" => Ok(Self::Error),
50            "Warning" | "warning" => Ok(Self::Warning),
51            "Info" | "info" => Ok(Self::Info),
52            s => Err(format!("Invalid severity: {s}")),
53        }
54    }
55}
56
57impl Severity {
58    /// Returns `true` if the severity is `Error`.
59    pub const fn is_error(&self) -> bool {
60        matches!(self, Self::Error)
61    }
62
63    /// Returns `true` if the severity is `Warning`.
64    pub const fn is_warning(&self) -> bool {
65        matches!(self, Self::Warning)
66    }
67
68    /// Returns `true` if the severity is `Info`.
69    pub const fn is_info(&self) -> bool {
70        matches!(self, Self::Info)
71    }
72
73    /// Returns the string representation of the severity.
74    pub const fn as_str(&self) -> &'static str {
75        match self {
76            Self::Error => "Error",
77            Self::Warning => "Warning",
78            Self::Info => "Info",
79        }
80    }
81
82    /// Returns the color to format the severity with.
83    pub const fn color(&self) -> Color {
84        match self {
85            Self::Error => Color::Red,
86            Self::Warning => Color::Yellow,
87            Self::Info => Color::White,
88        }
89    }
90}
91
92#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct Error {
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub source_location: Option<SourceLocation>,
97    #[serde(default, skip_serializing_if = "Vec::is_empty")]
98    pub secondary_source_locations: Vec<SecondarySourceLocation>,
99    pub r#type: String,
100    pub component: String,
101    pub severity: Severity,
102    #[serde(default, with = "serde_helpers::display_from_str_opt")]
103    pub error_code: Option<u64>,
104    pub message: String,
105    pub formatted_message: Option<String>,
106}
107
108impl Error {
109    /// Returns `true` if the error is an error.
110    pub const fn is_error(&self) -> bool {
111        self.severity.is_error()
112    }
113
114    /// Returns `true` if the error is a warning.
115    pub const fn is_warning(&self) -> bool {
116        self.severity.is_warning()
117    }
118
119    /// Returns `true` if the error is an info.
120    pub const fn is_info(&self) -> bool {
121        self.severity.is_info()
122    }
123}
124
125/// Tries to mimic Solidity's own error formatting.
126///
127/// <https://github.com/ethereum/solidity/blob/a297a687261a1c634551b1dac0e36d4573c19afe/liblangutil/SourceReferenceFormatter.cpp#L105>
128impl fmt::Display for Error {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        let mut short_msg = self.message.trim();
131        let fmtd_msg = self.formatted_message.as_deref().unwrap_or("");
132
133        if short_msg.is_empty() {
134            // if the message is empty, try to extract the first line from the formatted message
135            if let Some(first_line) = fmtd_msg.lines().next() {
136                // this is something like `ParserError: <short_message>`
137                if let Some((_, s)) = first_line.split_once(':') {
138                    short_msg = s.trim_start();
139                } else {
140                    short_msg = first_line;
141                }
142            }
143        }
144
145        // Error (XXXX): Error Message
146        styled(f, self.severity.color().bold(), |f| self.fmt_severity(f))?;
147        fmt_msg(f, short_msg)?;
148
149        let mut lines = fmtd_msg.lines();
150
151        if let Some(l) = lines.clone().next() {
152            if l.bytes().filter(|&b| b == b':').count() >= 3
153                && (l.contains(['/', '\\']) || l.contains(".sol"))
154            {
155                // This is an old style error message, like:
156                //     path/to/file:line:column: ErrorType: message
157                // We want to display this as-is.
158            } else {
159                // Otherwise, assume that the messages are the same until we find a source
160                // location.
161                lines.next();
162                while let Some(line) = lines.clone().next() {
163                    if line.contains(ARROW) {
164                        break;
165                    }
166                    lines.next();
167                }
168            }
169        }
170
171        // Format the main source location.
172        fmt_source_location(f, &mut lines)?;
173
174        // Format remaining lines as secondary locations.
175        while let Some(line) = lines.next() {
176            f.write_str("\n")?;
177
178            if let Some((note, msg)) = line.split_once(':') {
179                styled(f, Self::secondary_style(), |f| f.write_str(note))?;
180                fmt_msg(f, msg)?;
181            } else {
182                f.write_str(line)?;
183            }
184
185            fmt_source_location(f, &mut lines)?;
186        }
187
188        Ok(())
189    }
190}
191
192impl Error {
193    /// The style of the diagnostic severity.
194    pub fn error_style(&self) -> Style {
195        self.severity.color().bold()
196    }
197
198    /// The style of the diagnostic message.
199    pub fn message_style() -> Style {
200        Color::White.bold()
201    }
202
203    /// The style of the secondary source location.
204    pub fn secondary_style() -> Style {
205        Color::Cyan.bold()
206    }
207
208    /// The style of the source location highlight.
209    pub fn highlight_style() -> Style {
210        Style::new().fg(Color::Yellow)
211    }
212
213    /// The style of the diagnostics.
214    pub fn diag_style() -> Style {
215        Color::Yellow.bold()
216    }
217
218    /// The style of the source location frame.
219    pub fn frame_style() -> Style {
220        Style::new().fg(Color::Blue)
221    }
222
223    /// Formats the diagnostic severity:
224    ///
225    /// ```text
226    /// Error (XXXX)
227    /// ```
228    fn fmt_severity(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        f.write_str(self.severity.as_str())?;
230        if let Some(code) = self.error_code {
231            write!(f, " ({code})")?;
232        }
233        Ok(())
234    }
235}
236
237/// Calls `fun` in between [`Style::fmt_prefix`] and [`Style::fmt_suffix`].
238fn styled<F>(f: &mut fmt::Formatter<'_>, style: Style, fun: F) -> fmt::Result
239where
240    F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result,
241{
242    let enabled = yansi::is_enabled();
243    if enabled {
244        style.fmt_prefix(f)?;
245    }
246    fun(f)?;
247    if enabled {
248        style.fmt_suffix(f)?;
249    }
250    Ok(())
251}
252
253/// Formats the diagnostic message.
254fn fmt_msg(f: &mut fmt::Formatter<'_>, msg: &str) -> fmt::Result {
255    styled(f, Error::message_style(), |f| {
256        f.write_str(": ")?;
257        f.write_str(msg.trim_start())
258    })
259}
260
261/// Colors a Solidity source location:
262///
263/// ```text
264/// --> /home/user/contract.sol:420:69:
265///     |
266/// 420 |       bad_code()
267///     |                ^
268/// ```
269fn fmt_source_location(f: &mut fmt::Formatter<'_>, lines: &mut std::str::Lines<'_>) -> fmt::Result {
270    // --> source
271    if let Some(line) = lines.next() {
272        f.write_str("\n")?;
273        if let Some((left, loc)) = line.split_once(ARROW) {
274            f.write_str(left)?;
275            styled(f, Error::frame_style(), |f| f.write_str(ARROW))?;
276            f.write_str(loc)?;
277        } else {
278            f.write_str(line)?;
279        }
280    }
281
282    // get the next 3 lines
283    let Some(line1) = lines.next() else {
284        return Ok(());
285    };
286    let Some(line2) = lines.next() else {
287        f.write_str("\n")?;
288        f.write_str(line1)?;
289        return Ok(());
290    };
291    let Some(line3) = lines.next() else {
292        f.write_str("\n")?;
293        f.write_str(line1)?;
294        f.write_str("\n")?;
295        f.write_str(line2)?;
296        return Ok(());
297    };
298
299    // line 1, just a frame
300    fmt_framed_location(f, line1, None)?;
301
302    // line 2, frame and code; highlight the text based on line 3's carets
303    let hl_start = line3.find('^');
304    let highlight = hl_start.map(|start| {
305        let end = if line3.contains("^ (") {
306            // highlight the entire line because of "spans across multiple lines" diagnostic
307            line2.len()
308        } else if let Some(carets) = line3[start..].find(|c: char| c != '^') {
309            // highlight the text that the carets point to
310            start + carets
311        } else {
312            // the carets span the entire third line
313            line3.len()
314        }
315        // bound in case carets span longer than the code they point to
316        .min(line2.len());
317        (start.min(end)..end, Error::highlight_style())
318    });
319    fmt_framed_location(f, line2, highlight)?;
320
321    // line 3, frame and maybe highlight, this time till the end unconditionally
322    let highlight = hl_start.map(|i| (i..line3.len(), Error::diag_style()));
323    fmt_framed_location(f, line3, highlight)
324}
325
326/// Colors a single Solidity framed source location line. Part of [`fmt_source_location`].
327fn fmt_framed_location(
328    f: &mut fmt::Formatter<'_>,
329    line: &str,
330    highlight: Option<(Range<usize>, Style)>,
331) -> fmt::Result {
332    f.write_str("\n")?;
333
334    if let Some((space_or_line_number, rest)) = line.split_once('|') {
335        // if the potential frame is not just whitespace or numbers, don't color it
336        if !space_or_line_number.chars().all(|c| c.is_whitespace() || c.is_numeric()) {
337            return f.write_str(line);
338        }
339
340        styled(f, Error::frame_style(), |f| {
341            f.write_str(space_or_line_number)?;
342            f.write_str("|")
343        })?;
344
345        if let Some((range, style)) = highlight {
346            let Range { start, end } = range;
347            // Skip highlighting if the range is not valid unicode.
348            if !line.is_char_boundary(start) || !line.is_char_boundary(end) {
349                f.write_str(rest)
350            } else {
351                let rest_start = line.len() - rest.len();
352                f.write_str(&line[rest_start..start])?;
353                styled(f, style, |f| f.write_str(&line[range]))?;
354                f.write_str(&line[end..])
355            }
356        } else {
357            f.write_str(rest)
358        }
359    } else {
360        f.write_str(line)
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn fmt_unicode() {
370        let msg = "Invalid character in string. If you are trying to use Unicode characters, use a unicode\"...\" string literal.";
371        let e = Error {
372            source_location: Some(SourceLocation { file: "test/Counter.t.sol".into(), start: 418, end: 462 }),
373            secondary_source_locations: vec![],
374            r#type: "ParserError".into(),
375            component: "general".into(),
376            severity: Severity::Error,
377            error_code: Some(8936),
378            message: msg.into(),
379            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()),
380        };
381        let s = e.to_string();
382        eprintln!("{s}");
383        assert!(s.contains(msg), "\n{s}");
384    }
385
386    #[test]
387    fn only_formatted() {
388        let e = Error {
389            source_location: Some(SourceLocation { file: "test/Counter.t.sol".into(), start: 418, end: 462 }),
390            secondary_source_locations: vec![],
391            r#type: "ParserError".into(),
392            component: "general".into(),
393            severity: Severity::Error,
394            error_code: Some(8936),
395            message: String::new(),
396            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()),
397        };
398        let s = e.to_string();
399        eprintln!("{s}");
400        assert!(s.contains("Invalid character in string"), "\n{s}");
401    }
402
403    #[test]
404    fn solc_0_7() {
405        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":{}}"#;
406        let crate::CompilerOutput { errors, .. } = serde_json::from_str(output).unwrap();
407        assert_eq!(errors.len(), 1);
408        let s = errors[0].to_string();
409        eprintln!("{s}");
410        assert!(s.contains("test/Counter.t.sol:7:1"), "\n{s}");
411        assert!(s.contains("ABI coder v2"), "\n{s}");
412    }
413
414    #[test]
415    fn no_source_location() {
416        let error = r#"{"component":"general","errorCode":"6553","formattedMessage":"SyntaxError: The msize instruction cannot be used when the Yul optimizer is activated because it can change its semantics. Either disable the Yul optimizer or do not use the instruction.\n\n","message":"The msize instruction cannot be used when the Yul optimizer is activated because it can change its semantics. Either disable the Yul optimizer or do not use the instruction.","severity":"error","sourceLocation":{"end":173,"file":"","start":114},"type":"SyntaxError"}"#;
417        let error = serde_json::from_str::<Error>(error).unwrap();
418        let s = error.to_string();
419        eprintln!("{s}");
420        assert!(s.contains("Error (6553)"), "\n{s}");
421        assert!(s.contains("The msize instruction cannot be used"), "\n{s}");
422    }
423
424    #[test]
425    fn no_source_location2() {
426        let error = r#"{"component":"general","errorCode":"5667","formattedMessage":"Warning: Unused function parameter. Remove or comment out the variable name to silence this warning.\n\n","message":"Unused function parameter. Remove or comment out the variable name to silence this warning.","severity":"warning","sourceLocation":{"end":104,"file":"","start":95},"type":"Warning"}"#;
427        let error = serde_json::from_str::<Error>(error).unwrap();
428        let s = error.to_string();
429        eprintln!("{s}");
430        assert!(s.contains("Warning (5667)"), "\n{s}");
431        assert!(s.contains("Unused function parameter. Remove or comment out the variable name to silence this warning."), "\n{s}");
432    }
433
434    #[test]
435    fn stack_too_deep_multiline() {
436        let error = r#"{"sourceLocation":{"file":"test/LibMap.t.sol","start":15084,"end":15113},"type":"YulException","component":"general","severity":"error","errorCode":null,"message":"Yul exception:Cannot swap Variable _23 with Slot RET[fun_assertEq]: too deep in the stack by 1 slots in [ var_136614_mpos RET _23 _21 _23 var_map_136608_slot _34 _34 _29 _33 _33 _39 expr_48 var_bitWidth var_map_136608_slot _26 _29 var_bitWidth TMP[eq, 0] RET[fun_assertEq] ]\nmemoryguard was present.","formattedMessage":"YulException: Cannot swap Variable _23 with Slot RET[fun_assertEq]: too deep in the stack by 1 slots in [ var_136614_mpos RET _23 _21 _23 var_map_136608_slot _34 _34 _29 _33 _33 _39 expr_48 var_bitWidth var_map_136608_slot _26 _29 var_bitWidth TMP[eq, 0] RET[fun_assertEq] ]\nmemoryguard was present.\n   --> test/LibMap.t.sol:461:34:\n    |\n461 |             uint256 end = t.o - (t.o > 0 ? _random() % t.o : 0);\n    |                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n"}"#;
437        let error = serde_json::from_str::<Error>(error).unwrap();
438        let s = error.to_string();
439        eprintln!("{s}");
440        assert_eq!(s.match_indices("Cannot swap Variable _23").count(), 1, "\n{s}");
441        assert!(s.contains("-->"), "\n{s}");
442    }
443
444    #[test]
445    fn stack_too_deep_no_source_location() {
446        let error = r#"{"type":"CompilerError","component":"general","severity":"error","errorCode":null,"message":"Compiler error (/solidity/libyul/backends/evm/AsmCodeGen.cpp:63):Stack too deep. Try compiling with `--via-ir` (cli) or the equivalent `viaIR: true` (standard JSON) while enabling the optimizer. Otherwise, try removing local variables. When compiling inline assembly: Variable key_ is 2 slot(s) too deep inside the stack. Stack too deep. Try compiling with `--via-ir` (cli) or the equivalent `viaIR: true` (standard JSON) while enabling the optimizer. Otherwise, try removing local variables.","formattedMessage":"CompilerError: Stack too deep. Try compiling with `--via-ir` (cli) or the equivalent `viaIR: true` (standard JSON) while enabling the optimizer. Otherwise, try removing local variables. When compiling inline assembly: Variable key_ is 2 slot(s) too deep inside the stack. Stack too deep. Try compiling with `--via-ir` (cli) or the equivalent `viaIR: true` (standard JSON) while enabling the optimizer. Otherwise, try removing local variables.\n\n"}"#;
447        let error = serde_json::from_str::<Error>(error).unwrap();
448        let s = error.to_string();
449        eprintln!("{s}");
450        assert_eq!(s.match_indices("too deep inside the stack.").count(), 1, "\n{s}");
451    }
452}