hemtt_error/
lib.rs

1#![deny(clippy::all, clippy::nursery)]
2#![warn(clippy::pedantic)]
3#![allow(clippy::cast_possible_truncation)]
4
5use std::{io::BufRead, path::Path};
6
7pub use thiserror;
8
9use colored::Colorize;
10use hemtt_tokens::{Position, Token};
11
12#[derive(Debug)]
13pub struct AppError {
14    pub brief: String,
15    pub details: Option<String>,
16    pub help: Option<String>,
17    pub source: Option<Box<Source>>,
18    pub trace: Vec<Source>,
19}
20
21pub enum DisplayStyle {
22    Info,
23    Warning,
24    Error,
25}
26
27impl AppError {
28    #[must_use]
29    pub fn short(&self) -> &str {
30        &self.brief
31    }
32
33    #[must_use]
34    pub fn long(&self, style: &DisplayStyle) -> String {
35        format!(
36            "{}{}\n\n{}{}{}",
37            match style {
38                DisplayStyle::Info => format!("{}: ", "info".bright_blue()).bold(),
39                DisplayStyle::Warning => format!("{}: ", "warning".bright_yellow()).bold(),
40                DisplayStyle::Error => format!("{}: ", "error".bright_red()).bold(),
41            },
42            self.brief.bold(),
43            self.source().unwrap_or_default(),
44            {
45                let details = self.details.clone().unwrap_or_default();
46                if details.is_empty() {
47                    String::new()
48                } else {
49                    format!("{}: {details}\n", "details".bright_blue())
50                }
51            },
52            {
53                let help = self.help.clone().unwrap_or_default();
54                if help.is_empty() {
55                    String::new()
56                } else {
57                    format!("{}: {help}\n", "help".bright_yellow())
58                }
59            },
60        )
61    }
62
63    #[must_use]
64    pub fn source(&self) -> Option<String> {
65        let source = self.source.as_ref()?;
66        let trace = {
67            let mut trace = String::new();
68            for source in &self.trace {
69                if let Some(line) = source.lines.first() {
70                    trace.push_str(&format!(
71                        "    {}  {}:{}:{}\n{: >3} {} {}\n",
72                        "↓".bright_blue(),
73                        source
74                            .position
75                            .path()
76                            .replace('\\', "/")
77                            .trim_start_matches('/')
78                            .bold(),
79                        source.position.start().1 .0,
80                        source.position.start().1 .1,
81                        (source.position.start().1 .0).to_string().bright_blue(),
82                        "|".bright_blue(),
83                        line
84                    ));
85                }
86            }
87            trace
88        };
89        Some(format!(
90            "{}   {} {}:{}:{}\n{}\n",
91            trace,
92            "-->".blue(),
93            source
94                .position
95                .path()
96                .replace('\\', "/")
97                .trim_start_matches('/')
98                .bold(),
99            source.position.start().1 .0,
100            source.position.start().1 .1,
101            {
102                let bar = "    |".blue();
103                let mut lines = String::new();
104                for (i, line) in source.lines.iter().enumerate() {
105                    let linenum = source.position.start().1 .0 + i;
106                    lines.push_str(&format!(
107                        "{: >3} {} {}\n",
108                        linenum.to_string().blue(),
109                        "|".blue(),
110                        line
111                    ));
112                }
113                lines.push_str(&format!(
114                    "{} {:>offset$} {}",
115                    bar,
116                    "^".red(),
117                    source.note.red(),
118                    offset = source.position.start().1 .1
119                ));
120                format!("{bar}\n{lines}")
121            }
122        ))
123    }
124}
125
126impl<E> From<E> for AppError
127where
128    E: PrettyError,
129{
130    fn from(e: E) -> Self {
131        Self {
132            brief: e.brief(),
133            details: e.details(),
134            help: e.help(),
135            source: e.source(),
136            trace: e.trace(),
137        }
138    }
139}
140
141pub trait PrettyError: ToString {
142    fn brief(&self) -> String {
143        self.to_string()
144    }
145    fn details(&self) -> Option<String> {
146        None
147    }
148    fn help(&self) -> Option<String> {
149        None
150    }
151    fn source(&self) -> Option<Box<Source>> {
152        None
153    }
154    fn trace(&self) -> Vec<Source> {
155        Vec::new()
156    }
157}
158
159#[derive(Debug)]
160pub struct Source {
161    pub lines: Vec<String>,
162    pub position: Position,
163    pub note: String,
164}
165
166/// Read specific lines from a file
167///
168/// # Errors
169/// if the file cannot be read
170///
171/// # Panics
172/// if the lines are out of bounds
173///
174pub fn read_lines_from_file(
175    path: &Path,
176    start: usize,
177    end: usize,
178) -> Result<Vec<String>, std::io::Error> {
179    let file = std::fs::File::open(path)?;
180    let reader = std::io::BufReader::new(file);
181    let mut lines = reader.lines();
182    for _ in 1..start {
183        lines.next().unwrap().unwrap();
184    }
185    let mut ret = Vec::new();
186    for _ in 0..=(end - start) {
187        if let Some(x) = lines.next() {
188            ret.push(x.unwrap().trim_end().to_string());
189        }
190    }
191    Ok(ret)
192}
193
194/// Create a source object from a token
195///
196/// # Errors
197/// if the file cannot be read
198pub fn make_source(token: &Token, note: String) -> Result<Source, std::io::Error> {
199    Ok(Source {
200        lines: if token.source().path().starts_with('%') {
201            Vec::new()
202        } else {
203            read_lines_from_file(
204                Path::new(
205                    token
206                        .source()
207                        .path()
208                        .replace('\\', "/")
209                        .trim_start_matches('/'),
210                ),
211                token.source().start().1 .0,
212                token.source().end().1 .0,
213            )?
214        },
215        position: token.source().clone(),
216        note,
217    })
218}