leo_errors/common/
formatted.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use crate::{Backtraced, INDENT};
18
19use leo_span::{Span, source_map::LineContents, with_session_globals};
20
21use backtrace::Backtrace;
22use color_backtrace::{BacktracePrinter, Verbosity};
23use colored::Colorize;
24use itertools::Itertools;
25use std::fmt;
26
27/// Represents available colors for error labels.
28#[derive(Default, Clone, Debug, Hash, PartialEq, Eq)]
29pub enum Color {
30    #[default]
31    Red,
32    Yellow,
33    Blue,
34    Green,
35    Cyan,
36    Magenta,
37}
38
39impl Color {
40    /// Color `text` with `self` ane make it bold.
41    pub fn color_and_bold(&self, text: &str, use_colors: bool) -> String {
42        if use_colors {
43            match self {
44                Color::Red => text.bold().red().to_string(),
45                Color::Yellow => text.bold().yellow().to_string(),
46                Color::Blue => text.bold().blue().to_string(),
47                Color::Green => text.bold().green().to_string(),
48                Color::Cyan => text.bold().cyan().to_string(),
49                Color::Magenta => text.bold().magenta().to_string(),
50            }
51        } else {
52            text.to_string()
53        }
54    }
55}
56
57/// Represents error labels.
58#[derive(Clone, Debug, Hash, PartialEq, Eq)]
59pub struct Label {
60    msg: String,
61    span: Span,
62    color: Color,
63}
64
65impl Label {
66    pub fn new(msg: impl fmt::Display, span: Span) -> Self {
67        Self { msg: msg.to_string(), span, color: Color::default() }
68    }
69
70    pub fn with_color(mut self, color: Color) -> Self {
71        self.color = color;
72        self
73    }
74}
75
76/// Formatted compiler error type
77///     undefined value `x`
78///     --> file.leo: 2:8
79///      |
80///    2 | let a = x;
81///      |         ^
82///      |
83///      = help: Initialize a variable `x` first.
84/// Makes use of the same fields as a BacktracedError.
85#[derive(Clone, Debug, Hash, PartialEq, Eq)]
86pub struct Formatted {
87    /// The formatted error span information.
88    pub span: Span,
89    /// Additional spans with labels and optional colors for multi-span errors.
90    pub labels: Vec<Label>,
91    /// The backtrace to track where the Leo error originated.
92    pub backtrace: Box<Backtraced>,
93}
94
95impl Formatted {
96    /// Creates a backtraced error from a span and a backtrace.
97    #[allow(clippy::too_many_arguments)]
98    pub fn new_from_span<S>(
99        message: S,
100        help: Option<String>,
101        code: i32,
102        code_identifier: i8,
103        type_: String,
104        error: bool,
105        span: Span,
106        backtrace: Backtrace,
107    ) -> Self
108    where
109        S: ToString,
110    {
111        Self {
112            span,
113            labels: Vec::new(),
114            backtrace: Box::new(Backtraced::new_from_backtrace(
115                message.to_string(),
116                help,
117                code,
118                code_identifier,
119                type_,
120                error,
121                backtrace,
122            )),
123        }
124    }
125
126    /// Calls the backtraces error exit code.
127    pub fn exit_code(&self) -> i32 {
128        self.backtrace.exit_code()
129    }
130
131    /// Returns an error identifier.
132    pub fn error_code(&self) -> String {
133        self.backtrace.error_code()
134    }
135
136    /// Returns an warning identifier.
137    pub fn warning_code(&self) -> String {
138        self.backtrace.warning_code()
139    }
140
141    /// Returns a new instance of `Formatted` which has labels.
142    pub fn with_labels(mut self, labels: Vec<Label>) -> Self {
143        self.labels = labels;
144        self
145    }
146}
147
148/// Compute the start and end columns of the highlight for each line
149fn compute_line_spans(lc: &LineContents) -> Vec<(usize, usize)> {
150    let lines: Vec<&str> = lc.contents.lines().collect();
151    let mut byte_index = 0;
152    let mut line_spans = Vec::new();
153
154    for line in &lines {
155        let line_start = byte_index;
156        let line_end = byte_index + line.len();
157        let start = lc.start.saturating_sub(line_start);
158        let end = lc.end.saturating_sub(line_start);
159        line_spans.push((start.min(line.len()), end.min(line.len())));
160        byte_index = line_end + 1; // +1 for '\n'
161    }
162
163    line_spans
164}
165
166/// Print a gap or ellipsis between blocks of code
167fn print_gap(
168    f: &mut impl std::fmt::Write,
169    prev_last_line: Option<usize>,
170    first_line_of_block: usize,
171) -> std::fmt::Result {
172    if let Some(prev_last) = prev_last_line {
173        let gap = first_line_of_block.saturating_sub(prev_last + 1);
174        if gap == 1 {
175            // Single skipped line
176            writeln!(f, "{:width$} |", prev_last + 1, width = INDENT.len())?;
177        } else if gap > 1 {
178            // Multiple skipped lines
179            writeln!(f, "{:width$}...", "", width = INDENT.len() - 1)?;
180        }
181    }
182    Ok(())
183}
184
185/// Print a single line of code with connector and optional highlight
186#[allow(clippy::too_many_arguments)]
187fn print_code_line(
188    f: &mut impl std::fmt::Write,
189    line_num: usize,
190    line_text: &str,
191    connector: &str,
192    start: usize,
193    end: usize,
194    multiline: bool,
195    first_line: Option<usize>,
196    last_line: Option<usize>,
197    label: &Label,
198) -> std::fmt::Result {
199    let use_colors = std::env::var("NOCOLOR").unwrap_or_default().trim().to_owned().is_empty();
200
201    // Print line number, connector, and code
202    write!(f, "{:width$} | {} ", line_num, label.color.color_and_bold(connector, use_colors), width = INDENT.len())?;
203    writeln!(f, "{line_text}")?;
204
205    // Single-line highlight with caret
206    if !multiline && end > start {
207        writeln!(
208            f,
209            "{INDENT} |   {:start$}{} {}",
210            "",
211            label.color.color_and_bold(&"^".repeat(end - start), use_colors),
212            label.color.color_and_bold(&label.msg, use_colors),
213            start = start
214        )?;
215    }
216    // Multi-line highlight: only print underline on last line
217    else if multiline
218        && let (Some(first), Some(last)) = (first_line, last_line)
219        && line_num - first_line.unwrap() == last - first
220    {
221        let underline_len = (end - start).max(1);
222        writeln!(
223            f,
224            "{INDENT} | {:start$}{} {}",
225            label.color.color_and_bold("|", use_colors), // vertical pointer
226            label.color.color_and_bold(&"_".repeat(underline_len), use_colors), // underline
227            label.color.color_and_bold(&label.msg, use_colors), // message
228            start = start
229        )?;
230    }
231
232    Ok(())
233}
234
235/// Print the final underline for a multi-line highlight (Rust-style with `-`)
236fn print_multiline_underline(
237    f: &mut impl std::fmt::Write,
238    start_col: usize,
239    end_col: usize,
240    label: &Label,
241) -> std::fmt::Result {
242    let use_colors = std::env::var("NOCOLOR").unwrap_or_default().trim().to_owned().is_empty();
243    let underline_len = (end_col - start_col).max(1);
244    let underline = format!("{}-", "_".repeat(underline_len));
245
246    writeln!(
247        f,
248        "{INDENT} | {:start$}{} {}",
249        label.color.color_and_bold("|", use_colors), // vertical pointer
250        label.color.color_and_bold(&underline, use_colors), // colored underline + dash
251        label.color.color_and_bold(&label.msg, use_colors), // message
252        start = start_col
253    )
254}
255
256impl fmt::Display for Formatted {
257    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
258        let (kind, code) =
259            if self.backtrace.error { ("Error", self.error_code()) } else { ("Warning", self.warning_code()) };
260
261        let message = format!("{kind} [{code}]: {message}", message = self.backtrace.message,);
262
263        // To avoid the color enabling characters for comparison with test expectations.
264        if std::env::var("NOCOLOR").unwrap_or_default().trim().to_owned().is_empty() {
265            if self.backtrace.error {
266                writeln!(f, "{}", message.bold().red())?;
267            } else {
268                writeln!(f, "{}", message.bold().yellow())?;
269            }
270        } else {
271            writeln!(f, "{message}")?;
272        };
273
274        if let Some(source_file) = with_session_globals(|s| s.source_map.find_source_file(self.span.lo)) {
275            let line_contents = source_file.line_contents(self.span);
276
277            writeln!(
278                f,
279                "{indent     }--> {path}:{line_start}:{start}",
280                indent = INDENT,
281                path = &source_file.name,
282                // Report lines starting from line 1.
283                line_start = line_contents.line + 1,
284                // And columns - comments in some old code claims to report columns indexing from 0,
285                // but that doesn't appear to have been true.
286                start = line_contents.start + 1,
287            )?;
288
289            if self.labels.is_empty() {
290                // If there are no labels, just print the line contents which will point to the right location.
291                write!(f, "{line_contents}")?;
292            } else {
293                // If there are labels, we handle the printing manually. Something like:
294                //     |
295                //  50 | /         x
296                //  51 | |             :
297                //  52 | |                 u32,
298                //     | |___________________- `x` first declared here
299                //   ...
300                //  55 |           x: u32
301                //     |           ^^^^^^ struct field already declared
302
303                // Sort the labels by their source line number.
304                let labels = self
305                    .labels
306                    .iter()
307                    .filter_map(|label| {
308                        with_session_globals(|s| s.source_map.find_source_file(label.span.lo)).map(|source_file| {
309                            let lc = source_file.line_contents(label.span);
310                            (label.clone(), lc.line)
311                        })
312                    })
313                    .sorted_by_key(|(_, line)| *line)
314                    .map(|(label, _)| label)
315                    .collect_vec();
316
317                // Track the last printed line number to handle gaps between blocks
318                let mut prev_last_line: Option<usize> = None;
319
320                for label in labels {
321                    // Find the source file corresponding to this label's span
322                    let Some(source_file) = with_session_globals(|s| s.source_map.find_source_file(label.span.lo))
323                    else {
324                        continue;
325                    };
326
327                    // Get the line contents and offsets for this span
328                    let lc = source_file.line_contents(label.span);
329
330                    // Compute start and end columns of highlights for each line
331                    let line_spans = compute_line_spans(&lc);
332
333                    let first_line_of_block = lc.line + 1; // 1-based line number of the first line
334
335                    // Print a gap or ellipsis if there are skipped lines since previous label
336                    print_gap(f, prev_last_line, first_line_of_block)?;
337
338                    // Print a leading vertical margin only for the first label
339                    if prev_last_line.is_none() {
340                        writeln!(f, "{INDENT} |")?;
341                    }
342
343                    // Determine if this label spans multiple lines
344                    let multiline = line_spans.iter().any(|&(s, e)| e > s) && lc.contents.lines().count() > 1;
345
346                    // Identify first and last lines that have a highlight
347                    let first_line = line_spans.iter().position(|&(s, e)| e > s);
348                    let last_line = line_spans.iter().rposition(|&(s, e)| e > s);
349
350                    // Iterate over each line in the span
351                    for (i, (line_text, &(start, end))) in lc.contents.lines().zip(&line_spans).enumerate() {
352                        let line_num = lc.line + i + 1;
353
354                        // Choose connector symbol for multi-line highlights:
355                        // "/" for first line, "|" for continuation lines
356                        let connector = if multiline {
357                            match (first_line, last_line) {
358                                (Some(first), Some(_last)) => {
359                                    if i == first {
360                                        "/"
361                                    } else {
362                                        "|"
363                                    }
364                                }
365                                _ => " ",
366                            }
367                        } else {
368                            " "
369                        };
370
371                        // Print the code line with connector and optional single-line caret
372                        print_code_line(
373                            f, line_num, line_text, connector, start, end, multiline, first_line, last_line, &label,
374                        )?;
375                    }
376
377                    // If this was a multi-line highlight, print the final underline + message
378                    if multiline && let (Some(_), Some(last)) = (first_line, last_line) {
379                        // Start column: first highlighted character on the last line
380                        let start_col = line_spans[last].0;
381
382                        // End column: last highlighted character on the last line
383                        let end_col = line_spans[last].1;
384
385                        print_multiline_underline(f, start_col, end_col, &label)?;
386                    }
387
388                    // Update the previous last line to track gaps for the next label
389                    prev_last_line = Some(lc.line + lc.contents.lines().count());
390                }
391            }
392        }
393
394        if let Some(help) = &self.backtrace.help {
395            writeln!(
396                f,
397                "{INDENT     } |\n\
398                {INDENT     } = {help}",
399            )?;
400        }
401
402        let leo_backtrace = std::env::var("LEO_BACKTRACE").unwrap_or_default().trim().to_owned();
403        match leo_backtrace.as_ref() {
404            "1" => {
405                let mut printer = BacktracePrinter::default();
406                printer = printer.verbosity(Verbosity::Medium);
407                printer = printer.lib_verbosity(Verbosity::Medium);
408                let trace = printer.format_trace_to_string(&self.backtrace.backtrace).map_err(|_| fmt::Error)?;
409                write!(f, "\n{trace}")?;
410            }
411            "full" => {
412                let mut printer = BacktracePrinter::default();
413                printer = printer.verbosity(Verbosity::Full);
414                printer = printer.lib_verbosity(Verbosity::Full);
415                let trace = printer.format_trace_to_string(&self.backtrace.backtrace).map_err(|_| fmt::Error)?;
416                write!(f, "\n{trace}")?;
417            }
418            _ => {}
419        }
420
421        Ok(())
422    }
423}
424
425impl std::error::Error for Formatted {
426    fn description(&self) -> &str {
427        &self.backtrace.message
428    }
429}