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
166pub 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
194pub 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}