1use core::fmt::{self, Display, Formatter};
3use jaq_core::{compile, load};
4
5pub type FileReports<P = ()> = (load::File<String, P>, Vec<Report>);
7
8pub fn load_errors<P>(errs: load::Errors<&str, P>) -> Vec<FileReports<P>> {
10 use load::Error;
11
12 let errs = errs.into_iter().map(|(file, err)| {
13 let code = file.code;
14 let err = match err {
15 Error::Io(errs) => errs.into_iter().map(|e| report_io(code, e)).collect(),
16 Error::Lex(errs) => errs.into_iter().map(|e| report_lex(code, e)).collect(),
17 Error::Parse(errs) => errs.into_iter().map(|e| report_parse(code, e)).collect(),
18 };
19 (file.map_code(|s| s.into()), err)
20 });
21 errs.collect()
22}
23
24pub fn compile_errors<P>(errs: compile::Errors<&str, P>) -> Vec<FileReports<P>> {
26 let errs = errs.into_iter().map(|(file, errs)| {
27 let code = file.code;
28 let errs = errs.into_iter().map(|e| report_compile(code, e)).collect();
29 (file.map_code(|s| s.into()), errs)
30 });
31 errs.collect()
32}
33
34type StringColors = Vec<(String, Option<Color>)>;
35
36#[derive(Debug)]
38pub struct Report {
39 message: String,
41 labels: Vec<(core::ops::Range<usize>, StringColors, Color)>,
42}
43
44#[derive(Copy, Clone, Debug)]
46pub enum Color {
47 Red = 31,
49 Yellow = 33,
51}
52
53impl Color {
54 pub fn ansi(self, f: &mut Formatter, text: &dyn Display) -> fmt::Result {
56 write!(f, "\x1b[{}m{}", self as usize, text)?;
57 write!(f, "\x1b[{}m", 0)
58 }
59}
60
61fn report_io(code: &str, (path, error): (&str, String)) -> Report {
62 let path_range = load::span(code, path);
63 Report {
64 message: format!("could not load file {path}: {error}"),
65 labels: [(path_range, [(error, None)].into(), Color::Red)].into(),
66 }
67}
68
69fn report_lex(code: &str, (expected, found): load::lex::Error<&str>) -> Report {
70 let found = &found[..found.char_indices().nth(1).map_or(found.len(), |(i, _)| i)];
72
73 let found_range = load::span(code, found);
74 let found = match found {
75 "" => [("unexpected end of input".to_string(), None)].into(),
76 c => [("unexpected character ", None), (c, Some(Color::Red))]
77 .map(|(s, c)| (s.into(), c))
78 .into(),
79 };
80 let label = (found_range, found, Color::Red);
81
82 let labels = match expected {
83 load::lex::Expect::Delim(open) => {
84 let text = [("unclosed delimiter ", None), (open, Some(Color::Yellow))]
85 .map(|(s, c)| (s.into(), c));
86 Vec::from([(load::span(code, open), text.into(), Color::Yellow), label])
87 }
88 _ => Vec::from([label]),
89 };
90
91 Report {
92 message: format!("expected {}", expected.as_str()),
93 labels,
94 }
95}
96
97fn report_parse(code: &str, (expected, found): load::parse::Error<&str>) -> Report {
98 let found_range = load::span(code, found);
99
100 let found = if found.is_empty() {
101 "unexpected end of input"
102 } else {
103 "unexpected token"
104 };
105 let found = [(found.to_string(), None)].into();
106
107 Report {
108 message: format!("expected {}", expected.as_str()),
109 labels: Vec::from([(found_range, found, Color::Red)]),
110 }
111}
112
113fn report_compile(code: &str, (found, undefined): compile::Error<&str>) -> Report {
114 use compile::Undefined::Filter;
115 let found_range = load::span(code, found);
116 let wnoa = |exp, got| format!("wrong number of arguments (expected {exp}, found {got})");
117 let message = match (found, undefined) {
118 ("reduce", Filter(arity)) => wnoa("2", arity),
119 ("foreach", Filter(arity)) => wnoa("2 or 3", arity),
120 (_, undefined) => format!("undefined {}", undefined.as_str()),
121 };
122 let found = [(message.clone(), None)].into();
123
124 Report {
125 message,
126 labels: Vec::from([(found_range, found, Color::Red)]),
127 }
128}
129
130type CodeBlock = codesnake::Block<codesnake::CodeWidth<String>, Box<dyn Display>, Option<Color>>;
131
132pub type Paint = fn(&mut Formatter, &Option<Color>, &dyn Display) -> fmt::Result;
134
135struct FromFn<F>(F);
136
137fn from_fn<F: Fn(&mut Formatter) -> fmt::Result>(f: F) -> FromFn<F> {
138 FromFn(f)
139}
140
141impl<F: Fn(&mut Formatter) -> fmt::Result> Display for FromFn<F> {
142 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
143 (self.0)(f)
144 }
145}
146
147impl Report {
148 pub fn to_block(&self, idx: &codesnake::LineIndex, paint: Paint) -> CodeBlock {
150 use codesnake::{Block, CodeWidth, Label};
151 let labels = self.labels.iter().cloned().map(|(range, text, color)| {
152 Label::new(range)
153 .with_style(Some(color))
154 .with_text(Box::new(from_fn(move |f| {
155 text.iter().try_for_each(|(text, col)| paint(f, col, text))
156 })) as Box<dyn Display>)
157 });
158 let block = Block::new(idx, labels).unwrap();
159 block.with_paint(paint).map_code(|c| {
160 let c = c.replace('\t', " ");
161 let w = unicode_width::UnicodeWidthStr::width(&*c);
162 CodeWidth::new(c, core::cmp::max(w, 1))
163 })
164 }
165}
166
167pub struct FileReportsDisp<'a, P> {
169 file_reports: &'a FileReports<P>,
170 paint: Paint,
171 path: fn(&P) -> String,
172}
173
174impl<'a, P> FileReportsDisp<'a, P> {
175 pub fn new(file_reports: &'a FileReports<P>) -> Self {
179 Self {
180 file_reports,
181 paint: |f, _style, disp| disp.fmt(f),
182 path: |_| "".into(),
183 }
184 }
185
186 pub fn with_paint(mut self, paint: Paint) -> Self {
188 self.paint = paint;
189 self
190 }
191
192 pub fn with_path(mut self, path: fn(&P) -> String) -> Self {
194 self.path = path;
195 self
196 }
197}
198
199impl<'a, P> Display for FileReportsDisp<'a, P> {
200 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
201 let (file, reports) = &self.file_reports;
202 let path = (self.path)(&file.path);
203 let idx = codesnake::LineIndex::new(&file.code);
204 reports.iter().try_for_each(|e| {
205 writeln!(f, "Error: {}", e.message)?;
206 let block = e.to_block(&idx, self.paint);
207 writeln!(f, "{}{}", block.prologue(), path)?;
208 writeln!(f, "{}", block.space_vert())?;
209 writeln!(f, "{}{}", block, block.epilogue())
210 })
211 }
212}