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#[derive(
25 Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
26)]
27#[serde(rename_all = "lowercase")]
28pub enum Severity {
29 #[default]
31 Error,
32 Warning,
34 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 pub const fn is_error(&self) -> bool {
60 matches!(self, Self::Error)
61 }
62
63 pub const fn is_warning(&self) -> bool {
65 matches!(self, Self::Warning)
66 }
67
68 pub const fn is_info(&self) -> bool {
70 matches!(self, Self::Info)
71 }
72
73 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 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 pub const fn is_error(&self) -> bool {
111 self.severity.is_error()
112 }
113
114 pub const fn is_warning(&self) -> bool {
116 self.severity.is_warning()
117 }
118
119 pub const fn is_info(&self) -> bool {
121 self.severity.is_info()
122 }
123}
124
125impl 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 let Some(first_line) = fmtd_msg.lines().next() {
136 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 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 } else {
159 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 fmt_source_location(f, &mut lines)?;
173
174 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 pub fn error_style(&self) -> Style {
195 self.severity.color().bold()
196 }
197
198 pub fn message_style() -> Style {
200 Color::White.bold()
201 }
202
203 pub fn secondary_style() -> Style {
205 Color::Cyan.bold()
206 }
207
208 pub fn highlight_style() -> Style {
210 Style::new().fg(Color::Yellow)
211 }
212
213 pub fn diag_style() -> Style {
215 Color::Yellow.bold()
216 }
217
218 pub fn frame_style() -> Style {
220 Style::new().fg(Color::Blue)
221 }
222
223 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
237fn 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
253fn 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
261fn fmt_source_location(f: &mut fmt::Formatter<'_>, lines: &mut std::str::Lines<'_>) -> fmt::Result {
270 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 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 fmt_framed_location(f, line1, None)?;
301
302 let hl_start = line3.find('^');
304 let highlight = hl_start.map(|start| {
305 let end = if line3.contains("^ (") {
306 line2.len()
308 } else if let Some(carets) = line3[start..].find(|c: char| c != '^') {
309 start + carets
311 } else {
312 line3.len()
314 }
315 .min(line2.len());
317 (start.min(end)..end, Error::highlight_style())
318 });
319 fmt_framed_location(f, line2, highlight)?;
320
321 let highlight = hl_start.map(|i| (i..line3.len(), Error::diag_style()));
323 fmt_framed_location(f, line3, highlight)
324}
325
326fn 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 !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 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}